85 Commits
Author SHA1 Message Date
weixin 6c5c442928 fix(健康检测-用户数据): 睡眠折线图按阶段分段显示,各段间不再连续连线
每段睡眠数据(深睡/浅睡/清醒/零星小睡)之间插入 null 断点,
ECharts 折线图在 null 处自动断开,使不同睡眠阶段呈现为独立线段。
2026-06-30 16:39:25 +08:00
lianlonggang 2380e56266 fix(healthMonitor): 最后一次更新 2026-05-09 20:30:35 +08:00
lianlonggang fdce66d761 fix(healthMonitor): 修复健康监测模块中的组织机构数据显示和绑定功能
- 在sectoralData.data.ts中添加单位名称列的颜色渲染,三级单位显示为红色
- 在sectoralDataList.vue中添加组织代码长度限制,小于6位时禁用明细和导出功能
- 修复toolManagement组件中bindModal.vue的参数传递问题
- 修改toolManagement.data.ts中部门选择器的默认值和禁用状态
- 移除重复的单位名称表单项并优化搜索表单的组织机构选择逻辑
- 添加单位名称列到员工表格中并调整搜索表单的字段映射
2026-04-30 16:50:55 +08:00
weixin 34e1590382 update
1.修改发送位置时,地图引用组件错误问题
2026-04-10 09:43:35 +08:00
lianlonggang 7fea1a0d9f 修复手表数据日历展示问题,屏蔽非当前月的日期,切换日期发生年份变化时获取新年份月数据 2026-03-30 18:51:58 +08:00
weixin 8cf3ee079b update 2026-03-24 10:14:01 +08:00
weixin 388c6bdf4a update
1.修改健康银行访问链接,测试环境截取掉:+端口
2026-03-24 10:02:25 +08:00
weixin 6ca7141c5d update
1.修改健康银行访问链接
2026-03-24 09:22:41 +08:00
weixin 687fb37beb update
1.更换变量,修改外链打包后不生效问题
2026-03-12 16:10:15 +08:00
lianlonggang f269c42a58 增加环境变量VITE_OUT_URL 2026-03-12 15:16:18 +08:00
weixin 1cd7a43bc5 update
1.更新跳转外部链接
2026-03-12 14:58:38 +08:00
weixin 43e37e3c54 update 2026-02-12 10:32:18 +08:00
weixin e897357108 update:
1.修改通知消息bug
2.修改富文本编辑器组件
2026-02-11 16:42:30 +08:00
weixin a416e9d7df update:
1.消息通知
2.健康银行:积分管理、员工打卡监测、积分兑换记录
2026-02-06 18:03:53 +08:00
weixin ae3a3bcab6 update:
1.轮播图
2026-02-05 14:49:41 +08:00
lianlonggang 6ae5e62d42 日志文件下载功能,支持多实例 2026-01-29 19:38:11 +08:00
weixin 6e269ac987 update: 2026-01-20 15:33:45 +08:00
lianlonggang 31bdef36ea 短信发送功能,指定专家BUG修复 2026-01-16 16:58:52 +08:00
weixin 82ade3d3d4 update:
1.更新高德地图key
2026-01-12 16:31:02 +08:00
lianlonggang b9c7b6c1ac 短信发送功能,默认通知消息 2026-01-09 11:12:07 +08:00
weixin 99ba667bbb update: 2026-01-07 18:19:36 +08:00
weixin 9711d55edd update:
1.修改im聊天记录问题,更新im
2026-01-07 18:18:31 +08:00
weixin 2bd4cd399e update:
1.全科医生添加用户管理模块
2026-01-04 10:40:30 +08:00
lianlonggang d573fe5fba 调整短信发送对象顺序,默认指定用户 2025-12-30 14:30:05 +08:00
weixin b26398abef update:
1.更换手表导入模版及导入接口
2025-12-24 10:22:23 +08:00
weixin f0637d0fa1 update:
1.更换手表导入模版
2025-12-17 14:03:16 +08:00
weixin 0c3e495e53 update:1213问题修改 2025-12-13 18:43:00 +08:00
weixin be61c536b3 update 2025-12-05 16:01:45 +08:00
weixin bb41ae6afa update 2025-12-05 15:47:12 +08:00
weixin 62dc4e5e82 update
1.安眼健康体检添加体检详情
2025-12-05 15:23:49 +08:00
weixin 10e834f68e update
1.aed设备修改时,因为单位为空报错问题修改
2025-12-04 14:27:37 +08:00
weixin 0357c6aa8b update
1.修改提示语句
2025-12-03 17:47:17 +08:00
weixin 8a4113f5d4 update
1.添加健康小屋数据
2.天假体检数据
2025-12-03 16:12:26 +08:00
weixin f696bac580 update
1.更新im(专业人员/操作人员单点登陆,因为im模块)
2025-12-03 13:56:57 +08:00
weixin ad1da10c9c update
1.更新monitor
2025-12-03 10:04:37 +08:00
weixin 156d810329 update 2025-12-02 14:13:14 +08:00
weixin f96c3655df update
1.AED布点管理,布点位置调整为可手输可选点
2025-12-02 14:10:10 +08:00
weixin 9f6e34d7de update
1.AED布点管理,添加经纬度列
2025-12-02 09:04:44 +08:00
weixin 3d4177b647 update
1.替换修改救助人员接口
2.用户管理添加专业人员和操作人员模块
2025-11-27 16:24:38 +08:00
weixin 98a2680b63 update
1.应急就医-aed组网迁移
2025-11-27 13:30:19 +08:00
weixin ef852d13d6 update
1.更换地图秘钥
2025-11-12 13:37:32 +08:00
weixin 119f75f550 update
1.更换地图mapKey
2025-11-12 13:12:16 +08:00
weixin c4f9b4680f update
1.修改员工工号不限制输入格式(仅过滤空格)
2025-11-12 09:59:45 +08:00
weixin c94c567c60 update
1.更新im
2025-11-12 09:27:01 +08:00
weixin 557d013095 update 2025-11-11 18:11:15 +08:00
weixin 72074f59e0 update
1.修改im获取gourpId
2025-11-11 18:09:21 +08:00
weixin f4574a5df7 update
1.更改im配置
2025-11-11 17:16:33 +08:00
weixin 0b9c9d497e update
1.im聊天可以添加其他用户(专业人员)
2.更新im打包文件
2025-11-11 16:43:21 +08:00
lianlonggang 66a0c2f4d6 脚本调整,处理下载页面html的apiurl问题,开发环境暂时未处理 2025-11-05 17:46:03 +08:00
weixin 6125c6fc87 update
1.修改app下载地址
2025-11-05 15:30:46 +08:00
weixin e629dc3fb0 update
1.替换所有小助手字样为全科医师
2.添加app下载地址
2025-11-03 13:37:05 +08:00
weixin 80f7434a37 update
1.添加统一认证登陆
2025-10-31 17:52:21 +08:00
weixin 2e23b3641c update
1.更新h5
2025-10-09 09:19:41 +08:00
weixin 39d1ccf4bd update
1.修改monitor
2025-09-29 18:14:24 +08:00
weixin 3e92603025 update
1.新增员工时,添加字段
2025-09-19 18:36:07 +08:00
lianlonggang ae8e8e7e8f 增加脚本,支持docker环境变量的方式传入服务端接口地址 2025-09-08 09:49:51 +08:00
weixin 081eb910eb update
1.去掉新增员工时,工号必填
2025-08-28 09:57:00 +08:00
xf 5afe560654 update
1.系统管理-短信管理-短信发送
2025-08-21 10:21:10 +08:00
xf 8177bed6e2 update
1.员工编号、员工出生日期必填验证
2025-08-14 13:23:18 +08:00
xf 7510588122 update: 修改大屏体检数据假数据 2025-08-13 10:34:01 +08:00
xf c512766e95 update: 修改模拟用户判断 2025-08-06 15:56:51 +08:00
xf 6ed0a91f5d update: 隐藏坐席分组管理状态栏,应急中心添加管理员、大屏账号、编辑按钮权限 2025-08-06 14:07:24 +08:00
xf d85e40e83d update: 修改大屏假数据后台管理 2025-07-28 17:58:42 +08:00
xf 063b0a5efd update: 修改大屏假数据后台管理 2025-07-28 17:35:16 +08:00
xf 4fd7832eef update: 修改大屏假数据后台管理 2025-07-28 17:15:00 +08:00
weixin 11488335d1 update
1.更新专家端协议
2025-07-28 14:00:59 +08:00
weixin 5f9c7a073b update
1.更改协议
2025-07-28 11:47:29 +08:00
weixin 3db7b27796 update
1.修改隐私政策,用户协议
2.更改接口地址
2025-07-28 11:30:57 +08:00
weixin 751e07d747 update
1.im h5
2025-07-25 16:33:13 +08:00
weixin 985847eb7f update
1.im 聊天列表添加默认女头像
2.修改docker文件
2025-07-25 16:30:19 +08:00
lianlonggang f5bd3b9646 使用临时域名 2025-07-25 10:26:45 +08:00
weixin fb2a6b1c6f update
1.身份证脱敏
2025-07-23 09:05:57 +08:00
weixin 512d6cd5b9 update 2025-07-18 15:14:18 +08:00
xf 8beac20793 update: 登录密码加眼睛 2025-07-10 14:11:43 +08:00
weixin 3c49fc9d1e update 2025-07-09 18:46:09 +08:00
weixin 91f2c612e8 update
1.更新im
2.修改bug
2025-07-09 17:31:51 +08:00
xf 8a9ea4753e update: 登录密码加眼睛 2025-07-09 17:26:48 +08:00
weixin 78141a788f update
1.更新im
2.修改bug
2025-07-09 15:18:56 +08:00
weixin 308d9ae94e update 2025-07-08 13:16:28 +08:00
weixin 08b9144f8c update 2025-07-06 17:09:46 +08:00
weixin be946d3f0b update
1.更新im
2.修改最近提的问题
2025-07-06 15:15:37 +08:00
xf f0a17ff9f1 update: 登录密码加眼睛 2025-07-03 11:25:13 +08:00
xf d1736dfe25 update: 科室缩进 2025-07-03 11:08:48 +08:00
weixin 9cb6a0ad7a update
1.删除不需要的内容,修改部分样式
2025-07-03 09:39:26 +08:00
weixin 534e759350 update 2025-06-30 14:04:40 +08:00
2197 changed files with 30431 additions and 353339 deletions
+7 -4
View File
@@ -11,13 +11,13 @@ VITE_PUBLIC_PATH = /
VITE_DROP_CONSOLE = false
#后台接口父地址(必填)
VITE_GLOB_API_URL=http://cqyt.dev.yg.dt.io
#VITE_GLOB_API_URL=http://192.168.1.20
VITE_GLOB_API_URL=https://xj-api.mcrm.vip:8888
#VITE_GLOB_API_URL=http://192.168.1.223
#VITE_GLOB_API_URL=http://cqyt.test.yg.dt.io
#后台接口全路径地址(必填)
VITE_GLOB_DOMAIN_URL=http://cqyt.dev.yg.dt.io
#VITE_GLOB_DOMAIN_URL=http://192.168.1.20
VITE_GLOB_DOMAIN_URL=https://xj-api.mcrm.vip:8888
#VITE_GLOB_DOMAIN_URL=http://192.168.1.223
#VITE_GLOB_DOMAIN_URL=http://cqyt.test.yg.dt.io
# 接口前缀
VITE_GLOB_API_URL_PREFIX=
@@ -30,3 +30,6 @@ VITE_EMERGENCY_SCREEN=http://emer.dev.yg.dt.io/
#VITE_APP_SUB_jeecg-app-1 = '//localhost:8092'
VITE_PLATFORM = 'YT'
# 跳转外部链接
VITE_GLOB_OUT_URL=https://console-jkglpt.iosp.ydpt.tech/hb/healthadmin/index.html?param=
+5 -2
View File
@@ -16,10 +16,10 @@ VITE_BUILD_COMPRESS = 'gzip'
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
#后台接口父地址(必填)
VITE_GLOB_API_URL=https://api.cqygjk.com
VITE_GLOB_API_URL=https://xj-api.mcrm.vip:8888
#后台接口全路径地址(必填)
VITE_GLOB_DOMAIN_URL=https://api.cqygjk.com
VITE_GLOB_DOMAIN_URL=https://xj-api.mcrm.vip:8888
#食堂接口前缀
VITE_GLOB_ST_DOMAIN_URL=https://yyjk.cqygjk.com/gateway
#西安、银川应急大屏
@@ -39,3 +39,6 @@ VITE_LEGACY = false
# 油田平台CODE
VITE_PLATFORM = 'YT'
# 跳转外部链接
VITE_GLOB_OUT_URL=https://console-jkglpt.iosp.ydpt.tech/hb/healthadmin/index.html?param=
+5 -2
View File
@@ -16,10 +16,10 @@ VITE_BUILD_COMPRESS = 'gzip'
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
#后台接口父地址(必填)
VITE_GLOB_API_URL=http://cqyt.test.yg.dt.io
VITE_GLOB_API_URL=http://api-xj.tri.dt.io
#后台接口全路径地址(必填)
VITE_GLOB_DOMAIN_URL=http://cqyt.test.yg.dt.io
VITE_GLOB_DOMAIN_URL=http://api-xj.tri.dt.io
#食堂接口前缀
VITE_GLOB_ST_DOMAIN_URL=http://101.201.149.156:9002/gateway/zhst
#西安、银川应急大屏
@@ -37,3 +37,6 @@ VITE_USE_PWA = false
VITE_LEGACY = false
VITE_PLATFORM = 'YT'
# 跳转外部链接
VITE_GLOB_OUT_URL=https://gstest.superwx.cn/healthapiadmin/index.html?param=
+13 -9
View File
@@ -2,15 +2,12 @@ FROM nginx
MAINTAINER jeecgos@163.com
VOLUME /tmp
ENV LANG en_US.UTF-8
ENV VITE_GLOB_API_URL https://xj-api.mcrm.vip:8888
ENV VITE_GLOB_DOMAIN_URL https://xj-api.mcrm.vip:8888
ENV VITE_GLOB_OUT_URL https://gstest.superwx.cn/healthapiadmin/index.html?param=
RUN echo "server { \
listen 80; \
location /jeecgboot/ { \
proxy_pass http://jeecg-boot-system:8080/jeecg-boot/; \
proxy_redirect off; \
proxy_set_header Host jeecg-boot-system; \
proxy_set_header X-Real-IP \$remote_addr; \
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; \
} \
#解决Router(mode: 'history')模式下,刷新路由地址不能找到页面的问题 \
location / { \
root /var/www/html/; \
@@ -26,5 +23,12 @@ RUN echo "server { \
&& mkdir -p /var/www/html
ADD dist/ /var/www/html/
# 添加运行时注入脚本
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# 启动脚本
ENTRYPOINT ["bash","/entrypoint.sh"]
EXPOSE 80
EXPOSE 443
+53
View File
@@ -0,0 +1,53 @@
#!/bin/sh
set -e
# 从环境变量获取 APP_SHORT_NAME
APP_SHORT_NAME="${VITE_GLOB_APP_SHORT_NAME:-CQYTHEALTHADMIN}"
CONFIG_VAR="__PRODUCTION__${APP_SHORT_NAME}__CONF__"
# 1. 创建运行时配置文件
cat > /var/www/html/config.js << EOF
// 动态注入运行时配置
window['$CONFIG_VAR'] = {
"VITE_GLOB_APP_TITLE": "${VITE_GLOB_APP_TITLE:-健康管理后台}",
"VITE_GLOB_APP_TITLE_JD": "${VITE_GLOB_APP_TITLE_JD:-健康干预后台}",
"VITE_GLOB_APP_SHORT_NAME": "${VITE_GLOB_APP_SHORT_NAME:-CqytHealthAdmin}",
"VITE_GLOB_APP_CAS_BASE_URL": "${VITE_GLOB_APP_CAS_BASE_URL:-http://cas.default.com/cas}",
"VITE_GLOB_APP_OPEN_SSO": ${VITE_GLOB_APP_OPEN_SSO:-false},
"VITE_GLOB_APP_OPEN_QIANKUN": ${VITE_GLOB_APP_OPEN_QIANKUN:-true},
"VITE_GLOB_ONLINE_VIEW_URL": "${VITE_GLOB_ONLINE_VIEW_URL:-http://fileview.jeecg.com/onlinePreview}",
"VITE_GLOB_API_URL": "${VITE_GLOB_API_URL:-http://default-api.com}",
"VITE_GLOB_DOMAIN_URL": "${VITE_GLOB_DOMAIN_URL:-http://default-domain.com}",
"VITE_GLOB_ST_DOMAIN_URL": "${VITE_GLOB_ST_DOMAIN_URL:-http://default-domain.com}",
"VITE_GLOB_API_URL_PREFIX": "${VITE_GLOB_API_URL_PREFIX:-}",
"VITE_GLOB_OUT_URL": "${VITE_GLOB_OUT_URL:-}"
};
// 冻结对象(可选,模拟生产行为)
Object.freeze(window['$CONFIG_VAR']);
Object.defineProperty(window, '$CONFIG_VAR', {
configurable: false,
writable: false
});
EOF
# 2. 修改 index.html
# - 在 </head> 前插入 <script src="/config.js">
# - 并注释或移除原始的 _app.config.js 加载
sed -i '/<head[ >]/a <script src="\/config.js" defer><\/script>' /var/www/html/index.html
# 3. 屏蔽原始 _app.config.js(通过注释 script 标签)
# 匹配 src="/_app.config.js" 开头,允许后面有 ?v=任意字符
sed -i 's|<script[^>]*src="/_app.config.js[^"]*"[^>]*></script>|<!-- 屏蔽构建时配置:<script src="/_app.config.js" /> -->|g' /var/www/html/index.html
## 在下载页面的静态文件中注入JS
# 1. 创建运行时配置文件
cat > /var/www/html/static/apps/configApp.js << EOF
// 动态注入运行时配置
window['downUrl'] = "${VITE_GLOB_API_URL:-http://default-api.com}";
EOF
sed -i '/<head[ >]/a <script src="configApp.js" defer><\/script>' /var/www/html/static/apps/experEnd.html
sed -i '/<head[ >]/a <script src="configApp.js" defer><\/script>' /var/www/html/static/apps/index.html
# 启动 Nginx
exec nginx -g "daemon off;"
+1 -1
View File
@@ -32,7 +32,7 @@
<script>
window._CONFIG = {};
window._AMapSecurityConfig = {
securityJsCode: "1b3b86585c863d22db1e7f7636a9e495",
securityJsCode: "54f1d830d2bb225c173b76f5780b65eb",
}
+1
View File
@@ -1 +1,2 @@
src/TUIKit/TUIPlugin/TUICallKit/*
src/TUIKit/**
+3 -2
View File
@@ -3,7 +3,7 @@
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "cross-env NODE_OPTIONS= vue-cli-service build",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"test:e2e": "vue-cli-service test:e2e",
"lint": "vue-cli-service lint",
@@ -68,6 +68,7 @@
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"typescript": "^4.1.5",
"vue-eslint-parser": "^8.3.0"
"vue-eslint-parser": "^8.3.0",
"webpack": "^4.47.0"
}
}
@@ -0,0 +1,85 @@
<h1 align="center"> TUICallKit </h1>
<p align="center">
<b> English </b> | <a href="https://github.com/tencentyun/TUICallKit/blob/main/Web/README.md"> 简体中文 </a>
</p>
<p align="center"> A Vue2 & Vue3 Voice & Video Calling UI Component, easily add calling capabilities to your web application.Vue2 version <a href="https://www.npmjs.com/package/@tencentcloud/call-uikit-vue2"> @tencentcloud/call-uikit-vue2 </a></p>
<div align="center">
<img src="https://img.shields.io/npm/v/@tencentcloud/call-uikit-vue">
<img src="https://img.shields.io/badge/Vue-%5E3.0.0-brightgreen">
<img src="https://img.shields.io/badge/support-docs%20%26%20demos-yellow">
<img src="https://img.shields.io/npm/l/@tencentcloud/call-uikit-vue">
<!-- https://shields.io/category/version - tag: docs/demos, H5, v1.0.3(changelog),
GitHub Release Date: -->
</div>
<img src="https://user-images.githubusercontent.com/57169560/205650396-476e0e20-42a3-493a-8e90-6f7ba50da83e.gif" style="width: 1000px; margin: 10px;" align="center">
## Features
- ⚡️ Supports C2C/Group/Voice/Video calls, switch calling type, select calling devices
- 🌟 3 lines of code to run through the test demo, 6 lines of code to complete the common ability to access
- 📱 Multi-device adaptation, support H5 webview
- 🛠 Ecology system, works with [TUIKit](https://www.tencentcloud.com/document/product/1047/50061) to initiate audio/video calls directly in [TIM](https://www.tencentcloud.com/document/product/1047/33513) sessions
- 🔥 Out-of-the-box TypeScript support, support for Vue3 `Composition API`
- 🌍 Cross-platform, support for Android, iOS, Web, applets, Flutter, uniapp, etc. [multiple development platforms](https://www.tencentcloud.com/document/product/647/35078)
- ☁️ Deploy on Tencent Cloud, end-to-end average latency < 300ms on international links
- 🤙🏻 Low lag, anti-packet loss rate over 80%, anti-network jitter over 1000ms, still smooth and stable in weak network environment
- 🌈 High calling quality, support 720P, 1080P HD quality, 70% packet loss can still running
## How to use
This is a documentation for the TUICallKit project. It provides instructions for using and integrating TUICallKit into your website or application.
Here are a few guidelines for using this component.
- Click [here](https://tcms-demo.tencentcloud.com/exp-center/index.html#/detail?scene=callkit) to try out 1v1 Voice and Video Call online.
- If you want to experience it in your dev mode, please read [Run the Vue3 Demo](https://github.com/tencentyun/TUICallKit/blob/main/Web/demos/basic-vue3/README.en.md) or [Run the Vue2 Demo](https://github.com/tencentyun/TUICallKit/blob/main/Web/demos/basic-vue2/README.en.md)
- If you want to install this component into your project, please read [TUICallKit Getting Started](https://www.tencentcloud.com/document/product/647/50993)
- If you want to modify the CSS Style, please read [UI Customization](https://www.tencentcloud.com/document/product/647/50997)
## Contents
```text
.
├── README.md
├── demos/basic-vue3/
├── demos/basic-vue2/
└── TUICallKit/
```
**demos/basic-vue3/**
- The `demos/basic-vue3/` directory contains the Vue3 basic demo of TUICallKit, which is integrated with all the features of the full TUICallKit component.
- A debug panel is integrated at the bottom of the demo page. You can enter your application information directly, please refer to [Run the Vue3 Demo](https://github.com/tencentyun/TUICallKit/blob/main/Web/demos/basic-vue3/README.en.md).
**demos/basic-vue2/**
- The `demos/basic-vue2/` directory contains the Vue2 basic demo of TUICallKit, which is integrated with all the features of the full TUICallKit component.
- A debug panel is integrated at the bottom of the demo page. You can enter your application information directly, please refer to [Run the Vue2 Demo](https://github.com/tencentyun/TUICallKit/blob/main/Web/demos/basic-vue2/README.en.md).
**TUICallKIt/**
- The `TUICallKIt` directory contains the source code of TUICallKit. The entry file is `index.ts`.
- It is recommended to use a packaging method to directly import TUICallKit into your project, as outlined in the [TUICallKit Getting Started](https://www.tencentcloud.com/document/product/647/50993)
. You can also copy the files directly into your project for component import. For detailed instructions on how to integrate the source code, please refer to the [UI Customization](https://www.tencentcloud.com/document/product/647/50997).
<!-- - For the changelog of the SDK, see [Release Notes (Web)](https://www.tencentcloud.com/document/product/647/50997). -->
## Contact Us
- If you have questions, see [FAQs](https://www.tencentcloud.com/document/product/647/51024)
- To report bugs in our sample code, please create an issue.
- Communication & Feedback
Welcome to join our Telegram Group to communicate with our professional engineers! We are more than happy to hear from you~
Click to join: [https://t.me/+EPk6TMZEZMM5OGY1](https://t.me/+EPk6TMZEZMM5OGY1)
Or scan the QR code
<img src="https://qcloudimg.tencent-cloud.cn/raw/79cbfd13877704ff6e17f30de09002dd.jpg" width="300px">
## License
ISC License © 2022-present, [Tencent](https://www.tencent.com/)
@@ -0,0 +1,91 @@
<h1 align="center"> TUICallKit </h1>
<p align="center">
<a href="https://github.com/tencentyun/TUICallKit/blob/main/Web/README.en.md"> English </a> | <b> 简体中文 </b>
</p>
<p align="center">Vue3 版本的音视频通话 UI 组件,通过编写几行代码,就可以为您的 Web 应用添加音视频通话功能。Vue2 请参见 <a href="https://www.npmjs.com/package/@tencentcloud/call-uikit-vue2"> @tencentcloud/call-uikit-vue2 </a></p>
<!-- <p>在线演示 | Demo 跑通 | 快速接入 </p> -->
<div align="center">
<img src="https://img.shields.io/npm/v/@tencentcloud/call-uikit-vue">
<img src="https://img.shields.io/badge/Vue-%5E3.0.0-brightgreen">
<img src="https://img.shields.io/badge/support-docs%20%26%20demos-yellow">
<img src="https://img.shields.io/npm/l/@tencentcloud/call-uikit-vue">
<!-- https://shields.io/category/version - tag: docs/demos, H5, v1.0.3(changelog),
GitHub Release Date: -->
</div>
<img src="https://user-images.githubusercontent.com/57169560/194707785-6d2e1aca-5ee7-427a-be62-19699578e684.gif" style="width: 1000px; margin: 10px;" align="center">
## 特性
<!-- - 底层使用 腾讯云即时通信与音视频能力,效果媲美微信 -->
<!-- - 📦 开箱即用 —— 未引入除 SDK 外其他 npm 依赖,有效控制代码体积 -->
- ⚡️ 功能全面 —— 支持单人/多人/音频/视频通话、支持视频转音频通话、支持自由选择通话设备
- 🌟 低门槛 —— 仅需 3 行代码即可跑通测试 Demo,6 行代码完成通用能力接入
- 📱 多设备适配 —— 支持 H5 1v1 通话、群组通话 布局
- 🎨 灵活样式 —— 组件开源,可复用逻辑,自定义 UI 样式
- 🛠 优秀生态 —— 与 [TUIKit](https://cloud.tencent.com/document/product/269/79737) 协同使用,可以在 [TIM](https://cloud.tencent.com/document/product/269) 会话中直接发起音视频通话
- 🔥 先进技术栈 —— `TypeScript` 类型,支持 Vue3 `Composition API`
- 🌍 跨平台 —— 支持 Android、iOS、Web、小程序、Flutter、UniApp 等[多个开发平台](https://cloud.tencent.com/document/product/647/78742)
- ☁️ 低延迟 —— 腾讯云全球链路资源储备,保证国际链路端到端平均时延 < 300ms
- 🤙🏻 低卡顿 —— 抗丢包率超过 80%、抗网络抖动超过 1000ms,弱网环境仍顺畅稳定
- 🌈 高品质 —— 支持 720P、1080P 高清画质,70% 丢包率仍可正常视频
## 使用指引
```text
我们团队即将在 Web 端丰富更多不同的组件,以满足您的开发需求。
希望您能抽出几分钟时间,为您自己的需求投上一票,我们将优先开发!
问卷地址:https://wj.qq.com/s2/11263124/1556/
```
为方便您的使用,本组件配套多篇使用指引:
- 如果您想在线体验通话效果,请访问 [1v1音视频通话体验馆](https://web.sdk.qcloud.com/component/experience-center/index.html#/detail?scene=callkit)
- 如果您想要调试通话效果,请阅读 [Vue3 Demo 快速跑通](https://github.com/tencentyun/TUICallKit/blob/main/Web/demos/basic-vue3/README.md) 或者 [Vue2 Demo 快速跑通](https://github.com/tencentyun/TUICallKit/blob/main/Web/demos/basic-vue2/README.md)
- 如果您想把我们的功能直接嵌入到您的项目中,请阅读 [快速接入 TUICallKit](https://cloud.tencent.com/document/product/647/78731)
- 如果您想要修改 UI 界面,请阅读 [TUICallKit 界面定制指引](https://cloud.tencent.com/document/product/647/81014)
## 目录说明
```text
.
├── README.md
├── demos/basic-vue3/
├── demos/basic-vue2/
└── TUICallKit/
```
**demos/basic-vue3/**
- 此文件夹下是 Vue3 基础版 demo,集成了完整的 TUICallKit 组件功能,可以直接搜索用户拨打音视频通话。
- demo 页面下方集成了调试面板,可直接输入您的应用信息,具体流程请参考:[Vue3 Demo 快速跑通](https://github.com/tencentyun/TUICallKit/blob/main/Web/demos/basic-vue3/README.md)。
**demos/basic-vue2/**
- 此文件夹下是 Vue2.7 基础版 demo,集成了完整的 TUICallKit 组件功能,可以直接搜索用户拨打音视频通话。
- demo 页面下方集成了调试面板,可直接输入您的应用信息,具体流程请参考:[Vue2 Demo 快速跑通](https://github.com/tencentyun/TUICallKit/blob/main/Web/demos/basic-vue2/README.md)。
**TUICallKit/**
- 此文件夹下是 TUICallKit 组件源文件,支持 Vue3 与 Vue2.7+ 的项目直接引入。包含全部 UI 层与逻辑层代码,入口文件为 `index.ts`
- 推荐直接使用打包方式引入,如 [快速接入 TUICallKit](https://cloud.tencent.com/document/product/647/78731)。可直接复制此文件到您的工程中进行组件的引入,详细的源码接入组件过程请参考 [TUICallKit 界面定制指引](https://cloud.tencent.com/document/product/647/81014)。
## Changelog
版本更新历史请点击 [SDK 发布日志(Web)](https://cloud.tencent.com/document/product/647/80930)。
## 附录
- 如果你遇到了困难,可以先参阅 [常见问题](https://cloud.tencent.com/document/product/647/78769)
- 如果发现了示例代码的 bug,欢迎提交 issue;
- 欢迎加入 QQ 群:**646165204**,进行技术交流和反馈~
## License
ISC License © 2022-present, [Tencent](https://www.tencent.com/)
@@ -0,0 +1,63 @@
import LibGenerateTestUserSig from './lib-generate-test-usersig-es.min.js';
/**
* 腾讯云 SDKAppId,需要替换为您自己账号下的 SDKAppId。
*
* 进入腾讯云实时音视频[控制台](https://console.cloud.tencent.com/rav ) 创建应用,即可看到 SDKAppId,
* 它是腾讯云用于区分客户的唯一标识。
*/
let SDKAPPID = 0;
/**
* 签名过期时间,建议不要设置的过短
* <p>
* 时间单位:秒
* 默认时间:7 x 24 x 60 x 60 = 604800 = 7 天
*/
const EXPIRETIME = 604800;
/**
* 计算签名用的加密密钥,获取步骤如下:
*
* step1. 进入腾讯云实时音视频[控制台](https://console.cloud.tencent.com/rav ),如果还没有应用就创建一个,
* step2. 单击“应用配置”进入基础配置页面,并进一步找到“帐号体系集成”部分。
* step3. 点击“查看密钥”按钮,就可以看到计算 UserSig 使用的加密的密钥了,请将其拷贝并复制到如下的变量中
*
* 注意:该方案仅适用于调试Demo,正式上线前请将 UserSig 计算代码和密钥迁移到您的后台服务器上,以避免加密密钥泄露导致的流量盗用。
* 文档:https://cloud.tencent.com/document/product/647/17275#Server
*/
let SECRETKEY = '';
/*
* Module: GenerateTestUserSig
*
* Function: 用于生成测试用的 UserSig,UserSig 是腾讯云为其云服务设计的一种安全保护签名。
* 其计算方法是对 SDKAppID、UserID 和 EXPIRETIME 进行加密,加密算法为 HMAC-SHA256。
*
* Attention: 请不要将如下代码发布到您的线上正式版本的 App 中,原因如下:
*
* 本文件中的代码虽然能够正确计算出 UserSig,但仅适合快速调通 SDK 的基本功能,不适合线上产品,
* 这是因为客户端代码中的 SECRETKEY 很容易被反编译逆向破解,尤其是 Web 端的代码被破解的难度几乎为零。
* 一旦您的密钥泄露,攻击者就可以计算出正确的 UserSig 来盗用您的腾讯云流量。
*
* 正确的做法是将 UserSig 的计算代码和加密密钥放在您的业务服务器上,然后由 App 按需向您的服务器获取实时算出的 UserSig。
* 由于破解服务器的成本要高于破解客户端 App,所以服务器计算的方案能够更好地保护您的加密密钥。
*
* Referencehttps://cloud.tencent.com/document/product/647/17275#Server
*/
export function genTestUserSig({ userID, SDKAppID, SecretKey }) {
if (SDKAppID) SDKAPPID = SDKAppID;
if (SecretKey) SECRETKEY = SecretKey;
const generator = new LibGenerateTestUserSig(SDKAPPID, SECRETKEY, EXPIRETIME);
const userSig = generator.genTestUserSig(userID);
return {
SDKAppID: SDKAPPID,
userSig,
};
}
export default genTestUserSig;
File diff suppressed because one or more lines are too long
@@ -0,0 +1,38 @@
{
"name": "@tencentcloud/call-uikit-vue",
"version": "2.5.2",
"main": "./tuicall-uikit-vue.umd.js",
"module": "./tuicall-uikit-vue.es.js",
"types": "./types/index.d.ts",
"description": "An Open-source Voice & Video Calling UI Component Based on Tencent Cloud Service.",
"homepage": "https://cloud.tencent.com/document/product/647/78731",
"license": "ISC",
"repository": {
"type": "git",
"url": "https://github.com/tencentyun/TUICallKit.git",
"directory": "Web"
},
"dependencies": {
"tuicall-engine-webrtc": "^1.6.1"
},
"bugs": {
"url": "https://github.com/tencentyun/TUICallKit/issues"
},
"keywords": [
"uikit",
"call",
"vue3",
"tencent",
"chat",
"vue",
"vue2",
"video",
"audio",
"voice",
"callkit",
"语音",
"视频",
"电话",
"通话"
]
}
@@ -0,0 +1,241 @@
<template>
<div v-if="callStatus !== CallStatus.IDLE" id="tui-call-kit-id" :class="[bodyStyle, miniMizedDeskStyle, mobileVideoStyle, mobileAudioStyle]">
<SingleCall v-if="!isGroupCall" class="singCall" :allowed-full-screen="allowedFullScreen"></SingleCall>
<GroupCall v-else class="singCall"></GroupCall>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, toRefs, watchEffect } from '../adapter-vue';
import SingleCall from './components/SingleCall/SingleCall.vue';
import GroupCall from './components/GroupCall/GroupCall.vue';
import { PermitTip } from './components/common/PermitTip/PermitTip';
import { Toast } from './components/common/Toast';
import { TUIGlobal, TUIStore, StoreName, TUICallKitServer, NAME, CallStatus, CallMediaType } from '../TUICallService/index';
const isMobile = !TUIGlobal.isPC;
const callStatus = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS));
const isGroupCall = ref(TUIStore.getData(StoreName.CALL, NAME.IS_GROUP));
const bodyStyle = isMobile ? 'TUICallKit-mobile transition-animation' : 'TUICallKit-desktop';
const miniMizedDeskStyle = ref('');
const mobileAudioStyle = ref('');
const mobileVideoStyle = ref('');
const isShowFloatWindow = ref(false);
const callType = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE));
const props = withDefaults(
defineProps<{
beforeCalling?: (...args: any[]) => void;
afterCalling?: (...args: any[]) => void;
onMinimized?: (...args: any[]) => void;
onMessageSentByMe?: (...args: any[]) => void;
kickedOut?: (...args: any[]) => void;
statusChanged?: (...args: any[]) => void;
allowedMinimized?: boolean;
allowedFullScreen?: boolean;
videoDisplayMode?: string;
videoResolution?: string;
}>(),
{
allowedMinimized: false,
allowedFullScreen: true,
videoDisplayMode: 'cover',
videoResolution: '480p',
}
);
const {
beforeCalling,
afterCalling,
onMinimized,
onMessageSentByMe,
videoDisplayMode,
videoResolution,
kickedOut,
statusChanged,
allowedMinimized,
} = toRefs(props);
const handleCallStatusChange = (value) => {
callStatus.value = value;
};
const handleIsGroupChange = (value) => {
isGroupCall.value = value;
};
const handleToastInfoChange = (value) => {
if (value.text) {
showToast(value.text, value.type || 'info');
}
};
const handleCallMediaTypeChange = (value) => {
callType.value = value;
if (isMobile && isShowFloatWindow.value) {
mobileVideoStyle.value = 'miniMized-mobile-audio';
mobileAudioStyle.value = '';
}
if (isMobile && !isShowFloatWindow.value) {
mobileAudioStyle.value = 'mobile-audio';
}
};
const handleIsMinizedChange = (value) => {
isShowFloatWindow.value = value;
// 开启悬浮窗
if (value) {
if (!isMobile) {
miniMizedDeskStyle.value = 'miniMized';
} else {
if (callType.value === CallMediaType.AUDIO) {
mobileAudioStyle.value = 'miniMized-mobile-audio';
} else {
mobileVideoStyle.value = 'miniMized-mobile-video';
}
}
} else {
// 关闭悬浮窗
mobileAudioStyle.value = 'mobile-audio';
miniMizedDeskStyle.value = '';
mobileVideoStyle.value = '';
}
};
const handlePermissionErrorTipChange = (data) => {
if (data && TUIGlobal.isPC) {
try {
PermitTip.show();
} catch (error) {
console.debug(error);
}
}
};
watchEffect(() => {
TUICallKitServer.setCallback({
beforeCalling: beforeCalling && beforeCalling.value,
afterCalling: afterCalling && afterCalling.value,
onMinimized: onMinimized && onMinimized.value,
onMessageSentByMe: onMessageSentByMe && onMessageSentByMe.value,
kickedOut: kickedOut && kickedOut.value,
statusChanged: statusChanged && statusChanged.value,
});
});
onMounted(() => {
if (allowedMinimized.value) {
TUICallKitServer.enableFloatWindow(allowedMinimized.value);
}
TUICallKitServer.setVideoDisplayMode(videoDisplayMode.value as any);
TUICallKitServer.setVideoResolution(videoResolution.value as any);
TUIStore.watch(
StoreName.CALL,
{
[NAME.CALL_STATUS]: handleCallStatusChange,
[NAME.IS_GROUP]: handleIsGroupChange,
[NAME.TOAST_INFO]: handleToastInfoChange,
[NAME.CALL_MEDIA_TYPE]: handleCallMediaTypeChange,
[NAME.SHOW_PERMISSION_TIP]: handlePermissionErrorTipChange,
},
{
notifyRangeWhenWatch: NAME.MYSELF,
}
);
TUIStore.watch(StoreName.CALL, {
[NAME.IS_MINIMIZED]: handleIsMinizedChange,
});
});
function showToast(value: string, type: string) {
switch (type) {
case 'info':
Toast?.info(value as any);
break;
case 'error':
Toast?.error(value as any);
break;
default:
break;
}
}
onUnmounted(() => {
TUIStore.unwatch(StoreName.CALL, {
[NAME.CALL_STATUS]: handleCallStatusChange,
[NAME.IS_GROUP]: handleIsGroupChange,
[NAME.TOAST_INFO]: handleToastInfoChange,
[NAME.IS_MINIMIZED]: handleIsMinizedChange,
[NAME.SHOW_PERMISSION_TIP]: handlePermissionErrorTipChange,
});
});
</script>
<style lang="scss">
.TUICallKit-mobile {
width: 100%;
height: 100%;
.singCall {
width: 100%;
height: 100%;
}
}
.transition-animation {
transform: translateY(-100%);
animation: slideInDown 0.5s ease forwards;
}
@keyframes slideInDown {
from {
transform: translateY(-100%);
}
to {
transform: translateY(0);
}
}
.TUICallKit-desktop {
margin: 0 auto;
position: relative;
position: relative;
border-radius: inherit;
width: 100%;
height: 100%;
color: white;
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
z-index: 12;
border-radius: 16px;
.singCall {
width: 100%;
height: 100%;
}
}
.mobile-audio {
background-color: white;
}
.miniMized {
width: 168px !important;
height: 56px !important;
overflow: visible !important;
}
.miniMized-mobile-audio {
width: 72px;
height: 72px;
position: fixed;
top: 40px;
right: 40px;
}
.miniMized-mobile-video {
width: 40%;
height: 30%;
position: fixed;
top: 40px;
right: 40px;
}
</style>
@@ -0,0 +1 @@
<svg t="1688998928445" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1609" width="10" height="10"><path d="M512 466.752L86.656 41.344a32 32 0 0 0-45.312 45.312L466.752 512 41.344 937.344a32 32 0 0 0 45.312 45.312L512 557.248l425.344 425.408a32 32 0 0 0 45.312-45.312L557.248 512l425.408-425.344a32 32 0 0 0-45.312-45.312L512 466.752z" fill="#707070" p-id="1610"></path></svg>

After

Width:  |  Height:  |  Size: 421 B

@@ -0,0 +1 @@
<?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 t="1689595194053" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2344" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 170.666667v85.333333a256 256 0 1 1-223.573333 131.2L213.930667 345.6A341.333333 341.333333 0 1 0 512 170.666667z" fill="#000000" opacity=".3" p-id="2345"></path></svg>

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 40 40" class="design-iconfont">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 20C0 8.95431 8.95431 0 20 0V0C31.0457 0 40 8.95431 40 20V20C40 31.0457 31.0457 40 20 40V40C8.95431 40 0 31.0457 0 20V20Z" fill="#19D255" />
<path d="M23.5397 16.9535C24.7172 18.131 25.4436 19.1544 28.0149 17.0938C30.5885 15.0308 28.5979 13.6354 27.4573 12.4955C26.1407 11.1806 21.2674 12.3951 16.4667 17.1935C11.6683 21.9919 10.4521 26.8677 11.7687 28.1844C12.9092 29.3249 14.3022 31.3156 16.3646 28.7437C18.4282 26.173 17.4041 25.446 16.2248 24.2691C15.4043 23.4444 17.1034 21.3853 18.8797 19.6066C20.656 17.8278 22.7174 16.1288 23.5397 16.9535Z" fill="white" />
</svg>

After

Width:  |  Height:  |  Size: 731 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 40 40" class="design-iconfont">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M0 20C0 8.95431 8.95431 0 20 0C31.0457 0 40 8.95431 40 20C40 31.0457 31.0457 40 20 40C8.95431 40 0 31.0457 0 20Z"
fill="#19D255" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M24.0421 14.8129C23.8814 14.0792 23.3684 13.5231 22.7291 13.3902C21.6674 13.2126 20.3495 13 17.6667 13C14.7319 13 13.5011 13.2009 12.5263 13.3669C11.9235 13.5295 11.4484 14.0688 11.2926 14.7675C11.153 15.6419 11 17.8298 11 19.6667C11 21.4724 11.1481 23.6461 11.2912 24.5205C11.4519 25.2542 11.9649 25.8103 12.6042 25.9431C13.666 26.1207 14.9839 26.3333 17.6667 26.3333C20.3495 26.3333 21.6674 26.1207 22.7305 25.9522C23.3726 25.8194 23.8891 25.26 24.0512 24.5231C24.1853 23.6461 24.3333 21.4724 24.3333 19.6667C24.3333 17.8609 24.1853 15.6872 24.0421 14.8129ZM12.6667 15.5C12.6667 15.0398 13.0398 14.6667 13.5 14.6667H14.3333C14.7936 14.6667 15.1667 15.0398 15.1667 15.5C15.1667 15.9602 14.7936 16.3333 14.3333 16.3333H13.5C13.0398 16.3333 12.6667 15.9602 12.6667 15.5ZM28.5 14.6667C27.0942 14.6667 26 16.547 26 17.6429V21.6905C26 22.9804 26.8008 24.6667 28.5 24.6667C29.2858 24.6667 29.3333 21.4524 29.3333 19.6667C29.3333 17.881 29.3333 14.6667 28.5 14.6667Z"
fill="#fff" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 40 40" class="design-iconfont">
<rect width="40" height="40" rx="20" fill="#DADADA" fill-opacity=".3" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.7221 12.9557C22.7001 12.9496 22.6779 12.944 22.6556 12.939C21.5553 12.7392 20.1895 12.5 17.4091 12.5C14.3676 12.5 13.092 12.726 12.0818 12.9127C11.4571 13.0957 10.9647 13.7024 10.8033 14.4884C10.6585 15.4721 10.5 17.9335 10.5 20C10.5 21.7343 10.6118 23.7703 10.7369 24.9409L22.7221 12.9557ZM12.755 27.1654C13.7704 27.3355 15.1033 27.5 17.4091 27.5C20.1658 27.5 21.5319 27.2649 22.6289 27.0761L22.6571 27.0712C23.3225 26.9218 23.8578 26.2925 24.0258 25.4634C24.1647 24.4769 24.3182 22.0315 24.3182 20C24.3182 18.5797 24.2432 16.957 24.1473 15.773L12.755 27.1654ZM13.0606 14.375C12.6004 14.375 12.2273 14.7481 12.2273 15.2083V15.4167C12.2273 15.8769 12.6004 16.25 13.0606 16.25H13.9848C14.4451 16.25 14.8182 15.8769 14.8182 15.4167V15.2083C14.8182 14.7481 14.4451 14.375 13.9848 14.375H13.0606ZM26.0455 17.7232C26.0455 16.4904 27.1794 14.375 28.6364 14.375C29.5 14.375 29.5 17.9911 29.5 20C29.5 22.0089 29.4508 25.625 28.6364 25.625C26.8754 25.625 26.0455 23.7279 26.0455 22.2768V17.7232Z" fill="#fff" />
<rect x="24.8574" y="11.8564" width="1.5" height="22" rx=".75" transform="rotate(45 24.8574 11.8564)" fill="#FE3C44" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 40 40" class="design-iconfont">
<rect width="40" height="40" rx="20" fill="#DADADA" fill-opacity=".3" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M24.0164 14.5395C23.8498 13.7141 23.3182 13.0884 22.6556 12.939C21.5553 12.7392 20.1895 12.5 17.4091 12.5C14.3676 12.5 13.092 12.726 12.0818 12.9127C11.4571 13.0957 10.9647 13.7024 10.8033 14.4884C10.6585 15.4721 10.5 17.9335 10.5 20C10.5 22.0315 10.6535 24.4769 10.8018 25.4605C10.9684 26.2859 11.5 26.9116 12.1625 27.061C13.2629 27.2608 14.6287 27.5 17.4091 27.5C20.1895 27.5 21.5553 27.2608 22.6571 27.0712C23.3225 26.9218 23.8578 26.2925 24.0258 25.4634C24.1647 24.4769 24.3182 22.0315 24.3182 20C24.3182 17.9685 24.1647 15.5231 24.0164 14.5395ZM12.2273 15.2083C12.2273 14.7481 12.6004 14.375 13.0606 14.375H13.9848C14.4451 14.375 14.8182 14.7481 14.8182 15.2083V15.4167C14.8182 15.8769 14.4451 16.25 13.9848 16.25H13.0606C12.6004 16.25 12.2273 15.8769 12.2273 15.4167V15.2083ZM28.6364 14.375C27.1794 14.375 26.0455 16.4904 26.0455 17.7232V22.2768C26.0455 23.7279 26.8754 25.625 28.6364 25.625C29.4508 25.625 29.5 22.0089 29.5 20C29.5 17.9911 29.5 14.375 28.6364 14.375Z"
fill="#fff" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,3 @@
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.4437 12.449C18.4437 14.1142 18.2336 15.3515 21.5089 15.7126C24.7875 16.0737 24.3666 13.6794 24.3662 12.0669C24.3649 10.2061 20.0602 7.61895 13.2726 7.6173C6.48662 7.61735 2.1789 10.205 2.17888 12.067C2.17887 13.68 1.75621 16.0726 5.03315 15.7123C8.31009 15.3537 8.10005 14.1155 8.09837 12.4495C8.10134 11.2862 10.7587 11.0316 13.2725 11.0298C15.7863 11.0281 18.4454 11.2844 18.4437 12.449Z" fill="#FE3C44"/>
</svg>

After

Width:  |  Height:  |  Size: 536 B

@@ -0,0 +1,3 @@
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.33337 9.33331H0.333374V0.99998C0.333374 0.63179 0.631851 0.333313 1.00004 0.333313L9.33338 0.333313V2.33331H3.74758L10.0405 8.62624L8.62627 10.0404L2.33337 3.74753V9.33331ZM18.3331 11.3333H20.3331V19.6666C20.3331 20.0348 20.0347 20.3333 19.6665 20.3333L11.3331 20.3333L11.3331 18.3333H16.9189L10.626 12.0403L12.0402 10.6261L18.3331 16.919V11.3333Z" fill="#101419"/>
</svg>

After

Width:  |  Height:  |  Size: 520 B

@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.66666 4.16668C6.66666 2.32834 8.16166 0.833344 9.99999 0.833344C11.8383 0.833344 13.3333 2.32834 13.3333 4.16668V9.16668C13.3333 11.005 11.8383 12.5 9.99999 12.5C8.16166 12.5 6.66666 11.005 6.66666 9.16668V4.16668ZM6.66666 18.3333C6.66666 17.8725 7.03999 17.5 7.49999 17.5H12.5C12.9608 17.5 13.3333 17.8725 13.3333 18.3333C13.3333 18.7942 12.9608 19.1667 12.5 19.1667H7.49999C7.03999 19.1667 6.66666 18.7942 6.66666 18.3333Z" fill="#101419"/>
<path d="M15.8333 8.3512V9.50825C15.8333 12.081 13.2217 15 9.99999 15C6.77833 15 4.16666 12.081 4.16666 9.50825V8.33334" stroke="#101419" stroke-width="1.66667" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 783 B

@@ -0,0 +1,3 @@
<svg width="15" height="21" viewBox="0 0 15 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.6247 0.719168C14.1934 0.374158 13.5641 0.44408 13.2191 0.875342L10.8168 3.83203C10.1789 3.02455 9.15387 2.50004 8.00001 2.50004C6.06976 2.50004 4.50001 3.96785 4.50001 5.77276V10.6819C4.50001 10.9563 4.5363 11.2229 4.60463 11.4778L3.388 12.9752C2.98449 12.3433 2.75001 11.6522 2.75001 10.9717V9.81819L2.74319 9.71556C2.6892 9.31213 2.32108 9.00001 1.87501 9.00001C1.39176 9.00001 1.00001 9.36632 1.00001 9.81819V10.9717L1.00466 11.1761C1.05557 12.2968 1.52161 13.4016 2.26939 14.3519L0.219137 16.8753L0.146861 16.9782C-0.112977 17.4017 -0.0227767 17.9624 0.375311 18.2809C0.806573 18.6259 1.43587 18.556 1.78088 18.1247L3.71661 15.7423C3.71849 15.7437 3.72038 15.745 3.72227 15.7464L4.2997 15.0246L8.28088 10.1247L14.7809 2.12473L14.8532 2.02191C15.113 1.59834 15.0228 1.03764 14.6247 0.719168ZM7.72499 13.9446L11.5 9.22582V10.6819C11.5 12.4868 9.93026 13.9546 8.00001 13.9546C7.90746 13.9546 7.81575 13.9512 7.72499 13.9446ZM6.60445 15.3453L5.50798 16.7159C6.29225 17.0136 7.13532 17.1818 8.00001 17.1818C11.6976 17.1818 15 14.1064 15 10.9717V9.83572L14.9932 9.73309C14.9392 9.32966 14.5711 9.01754 14.125 9.01754C13.6418 9.01754 13.25 9.38385 13.25 9.83572V10.9717L13.2445 11.1527C13.1138 13.3262 10.6589 15.5455 8.00001 15.5455C7.52424 15.5455 7.05501 15.4744 6.60445 15.3453ZM5.37501 18.8637C4.89201 18.8637 4.50001 19.2294 4.50001 19.6819C4.50001 20.1343 4.89201 20.5 5.37501 20.5H10.625C11.1089 20.5 11.5 20.1343 11.5 19.6819C11.5 19.2294 11.1089 18.8637 10.625 18.8637H5.37501Z" fill="#101419"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.66683 9.33337H3.3335V4.00004C3.3335 3.63185 3.63197 3.33337 4.00016 3.33337L9.3335 3.33337V4.66671H5.60963L9.8049 8.86199L8.86209 9.8048L4.66683 5.60952V9.33337ZM15.3333 10.6667H16.6667V16C16.6667 16.3682 16.3682 16.6667 16 16.6667L10.6667 16.6667L10.6667 15.3333H14.3905L10.1953 11.138L11.1381 10.1952L15.3333 14.3905V10.6667Z" fill="white"/>
<path d="M0.666667 1.33333C0.666667 0.965144 0.965144 0.666667 1.33333 0.666667H18.6667C19.0349 0.666667 19.3333 0.965144 19.3333 1.33333V18.6667C19.3333 19.0349 19.0349 19.3333 18.6667 19.3333H1.33333C0.965143 19.3333 0.666667 19.0349 0.666667 18.6667V1.33333Z" stroke="white" stroke-width="1.33333"/>
</svg>

After

Width:  |  Height:  |  Size: 801 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 40 40" class="design-iconfont">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 20C0 8.95431 8.95431 0 20 0C31.0457 0 40 8.95431 40 20C40 31.0457 31.0457 40 20 40C8.95431 40 0 31.0457 0 20Z" fill="#FE3C44" />
<path d="M25.4437 20.449C25.4437 22.1142 25.2336 23.3515 28.5089 23.7126C31.7875 24.0737 31.3666 21.6794 31.3662 20.0669C31.3649 18.2061 27.0602 15.619 20.2726 15.6173C13.4866 15.6173 9.1789 18.205 9.17888 20.067C9.17887 21.68 8.75621 24.0726 12.0332 23.7123C15.3101 23.3537 15.1 22.1155 15.0984 20.4495C15.1013 19.2862 17.7587 19.0316 20.2725 19.0298C22.7863 19.0281 25.4454 19.2844 25.4437 20.449Z" fill="#fff" />
</svg>

After

Width:  |  Height:  |  Size: 711 B

@@ -0,0 +1,5 @@
<svg color="red" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 40 40" class="design-iconfont">
<rect width="40" height="40" rx="20" fill="#DADADA" fill-opacity=".3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 12C19.4477 12 19 12.4477 19 13V19H13C12.4477 19 12 19.4477 12 20C12 20.5523 12.4477 21 13 21H19V27C19 27.5523 19.4477 28 20 28C20.5523 28 21 27.5523 21 27V21H27C27.5523 21 28 20.5523 28 20C28 19.4477 27.5523 19 27 19H21V13C21 12.4477 20.5523 12 20 12Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 513 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 40 40" class="design-iconfont">
<rect opacity=".3" width="40" height="40" rx="20" fill="#fff" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M20 11.5C18.0697 11.5 16.5 12.9678 16.5 14.7727V19.6818C16.5 20.2623 16.6623 20.8078 16.9468 21.281L23.4997 14.7281C23.4741 12.9437 21.9143 11.5 20 11.5ZM23.5 18.9704L19.5436 22.9269C19.693 22.9451 19.8453 22.9545 20 22.9545C21.9303 22.9545 23.5 21.4867 23.5 19.6818V18.9704ZM15.7321 22.4957C15.1207 21.7452 14.75 20.8727 14.75 20.0171V18.8636L14.7432 18.761C14.6892 18.3575 14.3211 18.0454 13.875 18.0454C13.3918 18.0454 13 18.4117 13 18.8636V20.0171L13.0047 20.2215C13.0611 21.4647 13.6284 22.6883 14.5245 23.7033L15.7321 22.4957ZM16.9482 25.5222L18.2094 24.261C18.7777 24.4715 19.3834 24.5909 20 24.5909C22.6589 24.5909 25.1138 22.3716 25.2445 20.1981L25.25 20.0171V18.8811C25.25 18.4293 25.6418 18.0629 26.125 18.0629C26.5711 18.0629 26.9392 18.3751 26.9932 18.7785L27 18.8811V20.0171C27 23.1518 23.6976 26.2272 20 26.2272C18.9256 26.2272 17.8845 25.9676 16.9482 25.5222ZM16.5 28.6818C16.5 28.2294 16.892 27.8636 17.375 27.8636H22.625C23.1089 27.8636 23.5 28.2294 23.5 28.6818C23.5 29.1343 23.1089 29.5 22.625 29.5H17.375C16.892 29.5 16.5 29.1343 16.5 28.6818Z"
fill="#fff" />
<rect x="27.249" y="11.7688" width="1.5" height="22" rx=".75" transform="rotate(45 27.249 11.7688)"
fill="#FE3C44" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 40 40" class="design-iconfont">
<rect width="40" height="40" rx="20" fill="#DADADA" fill-opacity=".3" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M16.6665 14.1667C16.6665 12.3284 18.1615 10.8334 19.9998 10.8334C21.8382 10.8334 23.3332 12.3284 23.3332 14.1667V19.1667C23.3332 21.005 21.8382 22.5 19.9998 22.5C18.1615 22.5 16.6665 21.005 16.6665 19.1667V14.1667ZM16.6665 28.3334C16.6665 27.8725 17.0398 27.5 17.4998 27.5H22.4998C22.9607 27.5 23.3332 27.8725 23.3332 28.3334C23.3332 28.7942 22.9607 29.1667 22.4998 29.1667H17.4998C17.0398 29.1667 16.6665 28.7942 16.6665 28.3334Z"
fill="#fff" />
<path
d="M25.8332 18.3512V19.5083C25.8332 22.0811 23.2215 25 19.9998 25C16.7782 25 14.1665 22.0811 14.1665 19.5083V18.3334"
stroke="#fff" stroke-width="1.66667" stroke-linecap="round" />
</svg>

After

Width:  |  Height:  |  Size: 937 B

@@ -0,0 +1,6 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.769231" y="0.769231" width="18.4615" height="18.4615" rx="0.769231" stroke="white" stroke-width="1.53846"/>
<path d="M11.5385 12.3077C11.5385 11.8829 11.8829 11.5385 12.3077 11.5385H19.2308V18.4615C19.2308 18.8864 18.8864 19.2308 18.4616 19.2308H11.5385V12.3077Z" stroke="white" stroke-width="1.53846"/>
<path d="M0.769287 0.769226L8.9723 8.92412" stroke="white" stroke-width="1.53846"/>
<path d="M9.4967 4.71716V9.4486H4.74146" stroke="white" stroke-width="1.53846" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 647 B

@@ -0,0 +1,6 @@
import MacPermitZhPng from './win/permit-zh.png';
import MacPermitEnPng from './win/permit-en.png';
import WinPermitZhPng from './win/permit-zh.png';
import WinPermitEnPng from './win/permit-en.png';
export { MacPermitZhPng, MacPermitEnPng, WinPermitZhPng, WinPermitEnPng };
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16" class="design-iconfont">
<path
d="M13.733 6.99996C13.7997 6.99996 13.933 6.99996 13.9997 6.93329C14.133 6.86663 14.2663 6.73329 14.333 6.59996C14.3997 6.46663 14.3997 6.26663 14.333 6.06663C14.2663 5.99996 14.2663 5.93329 14.1997 5.86663L10.1997 1.86663C9.93301 1.59996 9.53301 1.59996 9.26634 1.86663C8.99967 2.13329 8.99967 2.53329 9.26634 2.79996L12.133 5.66663H1.99967C1.59967 5.66663 1.33301 5.93329 1.33301 6.33329C1.33301 6.73329 1.59967 6.99996 1.99967 6.99996H13.733ZM13.9997 8.99996H2.26634C2.19967 8.99996 2.06634 8.99996 1.99967 9.06663C1.86634 9.13329 1.73301 9.26663 1.66634 9.39996C1.59967 9.53329 1.59967 9.73329 1.66634 9.93329C1.73301 9.99996 1.73301 10.0666 1.79967 10.1333L5.79967 14.1333C5.93301 14.2666 6.13301 14.3333 6.26634 14.3333C6.39967 14.3333 6.59967 14.2666 6.73301 14.1333C6.99967 13.8666 6.99967 13.4666 6.73301 13.2L3.86634 10.3333H13.9997C14.3997 10.3333 14.6663 10.0666 14.6663 9.66663C14.6663 9.26663 14.3997 8.99996 13.9997 8.99996Z"
fill="#fff" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 40 40" class="design-iconfont">
<rect width="40" height="40" rx="20" fill="#DADADA" fill-opacity=".3" />
<path d="M17.7311 15.7485C17.7311 15.4339 18.1333 15.289 18.3596 15.507L22.9808 19.6831C23.1314 19.828 23.1314 20.0453 22.9808 20.166L18.3341 24.3435C18.1086 24.5608 17.7063 24.3917 17.7063 24.102V22.3396C17.7063 22.1464 17.5303 22.0016 17.3296 22.0016C14.2147 22.074 12.4319 22.8707 10.649 24.5366C10.3726 24.778 9.89538 24.5366 10.0205 24.1986C11.1007 21.1565 12.9345 18.4042 17.7311 17.8006V15.7485Z" fill="#fff" />
<path d="M24.7603 17.0411C26.4103 18.6911 26.4328 21.3438 24.8106 22.9661C24.5788 23.1978 24.1999 23.1946 23.9642 22.9589C23.7284 22.7232 23.7252 22.3442 23.957 22.1124C25.1157 20.9537 25.0996 19.0589 23.9211 17.8803C23.6853 17.6446 23.6821 17.2656 23.9139 17.0339C24.1456 16.8021 24.5246 16.8053 24.7603 17.0411Z" fill="#fff" />
<path d="M26.8765 14.925C29.7051 17.7536 29.7437 22.3011 26.9627 25.0821C26.731 25.3138 26.352 25.3106 26.1163 25.0749C25.8806 24.8392 25.8773 24.4602 26.1091 24.2285C28.4266 21.911 28.3944 18.1214 26.0373 15.7642C25.8016 15.5285 25.7983 15.1495 26.0301 14.9178C26.2618 14.686 26.6408 14.6893 26.8765 14.925Z" fill="#fff" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1017 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,5 @@
<svg color="#fff" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 40 40" class="design-iconfont">
<rect width="40" height="40" rx="20" fill="#DADADA" fill-opacity=".3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 12C19.4477 12 19 12.4477 19 13V19H13C12.4477 19 12 19.4477 12 20C12 20.5523 12.4477 21 13 21H19V27C19 27.5523 19.4477 28 20 28C20.5523 28 21 27.5523 21 27V21H27C27.5523 21 28 20.5523 28 20C28 19.4477 27.5523 19 27 19H21V13C21 12.4477 20.5523 12 20 12Z" fill="#2c3e63"/>
</svg>

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1 @@
<svg t="1695370599296" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10085" data-spm-anchor-id="a313x.search_index.0.i0.5a6e3a81PbZoWN" width="200" height="200"><path d="M1005.824 868.437333a883.029333 883.029333 0 0 1-591.018667-258.816A883.029333 883.029333 0 0 1 155.818667 16.725333a75.093333 75.093333 0 0 0-62.634667 21.248L52.736 78.421333c-44.373333 44.373333-62.293333 108.714667-47.445333 169.813334A1043.797333 1043.797333 0 0 0 225.28 678.912l120.32 120.234667a1044.48 1044.48 0 0 0 426.837333 219.050666 179.626667 179.626667 0 0 0 170.24-47.445333l41.728-41.728a74.752 74.752 0 0 0 21.418667-60.586667z" fill="#43C491" p-id="10086"></path><path d="M241.92 420.864L57.258667 73.898667l58.026666-58.026667c20.48-20.48 53.845333-20.48 74.325334 0l191.573333 191.573333c20.48 20.48 20.48 53.845333 0 74.24L241.92 420.949333z m359.509333 359.594667l347.050667 184.490666 58.026667-58.026666a52.736 52.736 0 0 0 0-74.24L814.933333 641.109333a52.736 52.736 0 0 0-74.24 0L601.344 780.458667z" fill="#43C491" p-id="10087"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.5 6.5H9.5M9.5 6.5V1.5M9.5 6.5L14.5 1.5" stroke="white" stroke-width="1.7"/>
<path d="M1.5 9.5H6.5M6.5 9.5V14.5M6.5 9.5L1.5 14.5" stroke="white" stroke-width="1.7"/>
</svg>

After

Width:  |  Height:  |  Size: 281 B

@@ -0,0 +1 @@
<svg t="1695611070308" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5740" width="200" height="200"><path d="M624.64 174.08l71.68 71.68L880.64 61.44l81.92 81.92-184.32 184.32 71.68 71.68c0 30.72-25.6 56.32-56.32 56.32H624.64c-30.72 0-56.32-25.6-56.32-56.32V230.4c0-30.72 25.6-56.32 56.32-56.32zM174.08 624.64c0-30.72 25.6-56.32 56.32-56.32h168.96c30.72 0 56.32 25.6 56.32 56.32V793.6c0 30.72-25.6 56.32-56.32 56.32l-71.68-71.68-184.32 184.32-81.92-81.92 184.32-184.32-71.68-71.68z" p-id="5741"></path></svg>

After

Width:  |  Height:  |  Size: 553 B

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.5 6.5H9.5M9.5 6.5V1.5M9.5 6.5L14.5 1.5" stroke="white" stroke-width="1.7"/>
<path d="M1.5 9.5H6.5M6.5 9.5V14.5M6.5 9.5L1.5 14.5" stroke="white" stroke-width="1.7"/>
</svg>

After

Width:  |  Height:  |  Size: 281 B

@@ -0,0 +1,121 @@
<template>
<div>
<!-- 移动端呼叫状态 -->
<div v-if="callStatus === CallStatus.CALLING && isMobile" :class="[mobile, textColor]">
<div class="btn-operate">
<Reject v-if="!isSponsor" class="mobile-reject" :is-mobile="isMobile"></Reject>
<Accept v-if="!isSponsor" class="mobile-accept" :is-mobile="isMobile" :call-type="callType"> </Accept>
<InviteUser v-if="isSponsor" class="mobile-invite" :is-mobile="isMobile" ></InviteUser>
<Hangup v-if="isSponsor" :isGroupCall="isGroupCall" :callType="callType" :is-mobile="isMobile" :isClickable="isClickable"></Hangup>
</div>
</div>
<!-- 移动端通话状态 -->
<div v-if="callStatus === CallStatus.CONNECTED && isMobile" :class="[mobile, textColor]">
<div class="btn-operate">
<Timer :call-duration="callDuration" :call-type="callType"></Timer>
</div>
<div v-if="callType === CallMediaType.VIDEO" class="btn-operate">
<Microphone class="btn" :local-user-info="localUserInfo" :is-mobile="isMobile"></Microphone>
<Speaker class="btn" v-if="!isH5"></Speaker>
<CameraBox class="btn" :is-mobile="isMobile"></CameraBox>
</div>
<div class="btn-operate">
<Microphone class="btn" v-if="callType === CallMediaType.AUDIO" :local-user-info="localUserInfo" :is-mobile="isMobile"></Microphone>
<InviteUser class="btn" :is-mobile="isMobile" ></InviteUser>
<Hangup class="btn" :is-mobile="isMobile" :isGroupCall="isGroupCall" :callType="callType" :isClickable="isClickable"></Hangup>
<Speaker class="btn" v-if="callType === CallMediaType.AUDIO && !isH5"></Speaker>
</div>
</div>
<!-- 桌面端呼叫状态 -->
<div v-if="callStatus === CallStatus.CALLING && !isMobile" class="control-wrapper-desktop">
<div class="panel-button-area">
<Reject class="btn" v-if="!isSponsor" :is-mobile="isMobile"></Reject>
<Accept class="btn" v-if="!isSponsor" :is-mobile="isMobile" :call-type="callType"></Accept>
<CameraBox class="btn" v-if="callType === CallMediaType.VIDEO" :is-mobile="isMobile"></CameraBox>
<Microphone class="btn" v-if="isSponsor" :local-user-info="localUserInfo" :is-mobile="isMobile"></Microphone>
<InviteUser class="btn" v-if="isSponsor" :is-mobile="isMobile" ></InviteUser>
<Hangup class="btn" v-if="isSponsor" :isGroupCall="isGroupCall" :callType="callType" :is-mobile="isMobile" :isClickable="isClickable"></Hangup>
</div>
</div>
<!-- 桌面端通话状态 -->
<div v-if="callStatus === CallStatus.CONNECTED && !isMobile" class="control-wrapper-desktop">
<div class="panel-button-area">
<CameraBox class="btn" v-if="callType === CallMediaType.VIDEO" :is-mobile="isMobile"></CameraBox>
<Microphone class="btn" :local-user-info="localUserInfo" :is-mobile="isMobile"></Microphone>
<InviteUser class="btn" :is-mobile="isMobile" ></InviteUser>
<Hangup class="btn" :is-mobile="isMobile" :isGroupCall="isGroupCall" :callType="callType" :isClickable="isClickable"></Hangup>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from '../../../../adapter-vue';
import Hangup from '../../common/Button/Hangup.vue';
import Accept from '../../common/Button/Accept.vue';
import Reject from '../../common/Button/Reject.vue';
import Microphone from '../../common/Button/Microphone.vue';
import Speaker from '../../common/Button/Speaker.vue';
import CameraBox from '../../common/Button/Camera.vue';
import InviteUser from '../../common/Button/InviteUser.vue';
import Timer from '../../common/Button/Timer.vue';
import { TUIGlobal, TUIStore, StoreName, NAME, CallMediaType, CallStatus, CallRole } from '../../../../TUICallService/index';
const callRole = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE));
const callType = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE));
const callStatus = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS));
const isGroupCall = ref(TUIStore.getData(StoreName.CALL, NAME.IS_GROUP));
const callDuration = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_DURATION));
const isClickable = ref(TUIStore.getData(StoreName.CALL, NAME.IS_CLICKABLE));
const isMobile = !TUIGlobal.isPC;
const isH5 = TUIGlobal.isH5;
let isSponsor = callRole.value === CallRole.CALLER;
const mobile = 'control-wrapper-mobile';
const textColor = 'black';
const localUserInfo = ref(TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO));
const handleLocalUserInfoChange = (value) => {
localUserInfo.value = value;
};
const handleCallRoleChange = (value) => {
isSponsor = value === CallRole.CALLER;
callRole.value = value;
};
const handleIsGroupChange = (value) => {
isGroupCall.value = value;
};
const handleCallMediaTypeChange = (value) => {
callType.value = value;
};
const handleCallStatusChange = (value) => {
callStatus.value = value;
};
const handleCallDurationChange = (value) => {
callDuration.value = value;
};
const handleIsClickableChange = (value) => {
isClickable.value = value;
};
const watchOptions = {
[NAME.LOCAL_USER_INFO]: handleLocalUserInfoChange,
[NAME.CALL_ROLE]: handleCallRoleChange,
[NAME.IS_GROUP]: handleIsGroupChange,
[NAME.CALL_MEDIA_TYPE]: handleCallMediaTypeChange,
[NAME.CALL_STATUS]: handleCallStatusChange,
[NAME.CALL_DURATION]: handleCallDurationChange,
[NAME.IS_CLICKABLE]: handleIsClickableChange,
};
onMounted(() => {
TUIStore.watch(StoreName.CALL, watchOptions, { notifyRangeWhenWatch: NAME.MYSELF });
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CALL, watchOptions);
});
</script>
<style lang="scss" src="./style/index.scss" scoped></style>
@@ -0,0 +1,60 @@
//桌面端
.control-wrapper-desktop {
transform: translate(-50%);
position: absolute;
bottom: 0;
padding-bottom: 40px;
left: 50%;
width: 100%;
color: white;
border-radius: inherit;
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.4) 100%
);
display: flex;
flex-direction: column;
flex-wrap: wrap;
align-items: center;
user-select: none;
.panel-button-area {
display: flex;
flex-direction: row;
//flex-wrap: wrap;
justify-content: space-evenly;
margin: 30px 0;
min-width: 40%;
.btn-operate-desktop {
.call-operate {
display: flex;
flex-direction: column;
width: 40px;
height: 40px;
margin: 0 40px;
cursor: pointer;
}
img {
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
position: relative;
}
span {
display: block;
margin-top: 5px;
white-space: nowrap;
font-weight: 400;
font-size: 14px;
line-height: 20px;
text-align: center;
}
}
}
}
@@ -0,0 +1,2 @@
@import './desktop.scss';
@import './mobile.scss';
@@ -0,0 +1,46 @@
.control-wrapper-mobile {
position: absolute;
bottom: 20px;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.btn-operate {
display: flex;
justify-content: center;
align-items: center;
position: relative;
font-size: 16px;
letter-spacing: 0;
font-weight: 400;
margin-bottom: 10px;
.btn {
margin: 0 5vw;
}
.mobile-invite{
margin-right: 30px;
}
.mobile-reject{
margin: 0px 16vw 20px 16vw;
}
.mobile-accept{
margin: 0px 16vw 20px 16vw;
}
.mobile-switchAudioCall{
font-size: 14px;
}
}
}
.black{
color: black;
}
.white{
color: white;
}
@@ -0,0 +1,133 @@
<template>
<div :class="bodyStyle">
<SelectUser
:isShow="showSelectUser"
:isNeedSearch="true"
:userList="groupMemberList"
:isPC="!isMobile"
:total="memberCount"
@getMore="getMoreMemberList"
@complete="handleSelectedResult"
@search="handleSearch"
@update:isShow="handleCancel"
/>
<div v-if="callStatus === CallStatus.CALLING && callRole !== CallRole.CALLER">
<InviteInfo></InviteInfo>
</div>
<PusherPlayer v-if="callStatus === CallStatus.CONNECTED || callRole === CallRole.CALLER"></PusherPlayer>
<BtnOperate></BtnOperate>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from '../../../adapter-vue';
import { TUICallKitServer, TUIGlobal, TUIStore, StoreName, NAME, CallStatus, CallRole } from '../../../TUICallService/index';
import InviteInfo from './InviteInfo/InviteInfo.vue';
import BtnOperate from './BtnOperate/BtnOperate.vue';
import PusherPlayer from './PusherPlayer/PusherPlayer.vue';
import SelectUser from '../common/SelectUser/SelectUser.vue';
const callRole = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE));
const callStatus = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS));
const callType = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE));
const isMobile = !TUIGlobal.isPC;
const bodyStyle = isMobile ? '' : 'group-deskTop';
const localUserInfo = ref(TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO));
const remoteUserInfoList = ref(TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST));
const showSelectUser = ref(false);
const memberCount = ref(0);
const groupMemberList = ref([]);
const backupGroupMemberList = ref([]);
const offset = ref(0);
const count: number = 30;
onMounted(() => {
TUIStore.watch(StoreName.CALL, watchOptions, { notifyRangeWhenWatch: NAME.MYSELF });
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CALL, watchOptions);
});
const handleCallStatusChange = (value: CallStatus) => {
callStatus.value = value;
};
const handleCallMediaTypeChange = (value: any) => {
callType.value = value;
};
const handleCallRoleChange = (value: CallRole) => {
callRole.value = value;
};
const handleLocalUserInfoChange = (value: any) => {
localUserInfo.value = value;
};
const handleRemoteUserInfoChange = (value: any) => {
remoteUserInfoList.value = value;
};
const handleShowSelectUser = async (value: boolean) => {
showSelectUser.value = value;
if (showSelectUser.value) {
await getGroupMemberList();
const groupProfile = await TUICallKitServer.getGroupProfile();
memberCount.value = groupProfile.memberCount;
}
};
const watchOptions = {
[NAME.CALL_STATUS]: handleCallStatusChange,
[NAME.CALL_MEDIA_TYPE]: handleCallMediaTypeChange,
[NAME.CALL_ROLE]: handleCallRoleChange,
[NAME.SHOW_SELECT_USER]: handleShowSelectUser,
[NAME.LOCAL_USER_INFO]: handleLocalUserInfoChange,
[NAME.REMOTE_USER_INFO_LIST]: handleRemoteUserInfoChange,
};
const handleCancel = () => {
showSelectUser.value = false;
TUIStore.update(StoreName.CALL, NAME.SHOW_SELECT_USER, false);
offset.value = 0;
groupMemberList.value = [];
};
const getMoreMemberList = async () => {
offset.value += count;
await getGroupMemberList();
};
const getGroupMemberList = async () => {
const memberList = await TUICallKitServer.getGroupMemberList(count, offset.value);
const inCallUserIdList = [...remoteUserInfoList.value, localUserInfo.value].map((obj) => obj.userId);
groupMemberList.value.push(...memberList);
groupMemberList.value = groupMemberList.value.map((obj) => {
if (inCallUserIdList.includes(obj.userID)) {
obj = { ...obj, isDisabled: true };
}
return obj;
});
backupGroupMemberList.value = groupMemberList.value;
};
const handleSelectedResult = async (selectedUserInfoList: Array<any>) => {
try {
if (selectedUserInfoList.length <= 0) {
return;
}
showSelectUser.value = false;
TUIStore.update(StoreName.CALL, NAME.SHOW_SELECT_USER, false);
offset.value = 0;
const userIDList = selectedUserInfoList.map((obj) => obj.userID);
await TUICallKitServer.inviteUser({ userIDList });
groupMemberList.value = [];
} catch (error) {
console.debug(error);
}
};
const handleSearch = (searchValue: string) => {
if (searchValue) {
groupMemberList.value = groupMemberList.value.filter((obj) => {
return obj.userID.includes(searchValue) || obj.nick.includes(searchValue);
});
} else {
groupMemberList.value = backupGroupMemberList.value;
}
};
</script>
<style scoped>
.group-deskTop {
background: #00183c;
z-index: 0;
}
</style>
@@ -0,0 +1,127 @@
<template>
<div>
<!-- 移动端 -->
<div v-if="isMobile && remoteUsersList.length !== 0" :class="bodyStyle">
<InviteCallee :local-user-info="localUserInfo" :callerUserInfo="callerUserInfo" :remote-users-list="remoteUsersList"></InviteCallee>
</div>
<!-- 桌面端 -->
<div v-if="!isMobile && remoteUsersList.length !== 0" :class="bodyStyle">
<div class="singleInvite-message">
<div class="singleInvite-nick">
<div class="singleInvite-nick-span">{{ callerUserInfo.displayUserInfo }}</div>
</div>
<span class="singleInvite-tips">{{ callAlertMessage }}</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from '../../../../adapter-vue';
import { TUIGlobal, TUIStore, StoreName, NAME } from '../../../../TUICallService/index';
import InviteCallee from './mobile/InviteCallee.vue';
const isMobile = !TUIGlobal.isPC;
const bodyStyle = isMobile ? 'groupInvite-header-mobile' : 'groupInvite-header-desktop';
const callRole = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE));
const callType = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE));
const isGroupCall = ref(TUIStore.getData(StoreName.CALL, NAME.IS_GROUP));
const remoteUsersList = ref(TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST));
const callStatus = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS));
const callDuration = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_DURATION));
const callAlertMessage = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_TIPS));
const localUserInfo = ref(TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO));
const callerUserInfo = ref(TUIStore.getData(StoreName.CALL, NAME.CALLER_USER_INFO));
const handleCallerUserInfoChange = (value) => {
callerUserInfo.value = value;
};
const handleLocalUserInfoChange = (value) => {
localUserInfo.value = value;
};
const handleCallRoleChange = (value) => {
callRole.value = value;
};
const handleIsGroupChange = (value) => {
isGroupCall.value = value;
};
const handleCallMediaTypeChange = (value) => {
callType.value = value;
};
const handleRemoteUserInfoListChange = (value) => {
remoteUsersList.value = value;
};
const handleCallStatusChange = (value) => {
callStatus.value = value;
};
const handleCallDurationChange = (value) => {
callDuration.value = value;
};
const handleCallTipsChange = (value) => {
callAlertMessage.value = value;
};
const watchOptions = {
[NAME.CALLER_USER_INFO]: handleCallerUserInfoChange,
[NAME.LOCAL_USER_INFO]: handleLocalUserInfoChange,
[NAME.CALL_ROLE]: handleCallRoleChange,
[NAME.IS_GROUP]: handleIsGroupChange,
[NAME.CALL_MEDIA_TYPE]: handleCallMediaTypeChange,
[NAME.REMOTE_USER_INFO_LIST]: handleRemoteUserInfoListChange,
[NAME.CALL_STATUS]: handleCallStatusChange,
[NAME.CALL_DURATION]: handleCallDurationChange,
[NAME.CALL_TIPS]: handleCallTipsChange,
};
onMounted(() => {
TUIStore.watch(StoreName.CALL, watchOptions, { notifyRangeWhenWatch: NAME.MYSELF });
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CALL, watchOptions);
});
</script>
<style lang="scss" scoped>
.groupInvite-header-desktop {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
position: absolute;
left: 50%;
top: 42%;
transform: translate(-50%, -50%);
.singleInvite-message {
text-align: center;
.singleInvite-nick {
font-style: normal;
font-weight: 600;
font-size: 40px;
line-height: 56px;
display: flex;
align-items: center;
justify-content: center;
color: white;
.singleInvite-nick-span {
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
line-height: normal;
white-space: nowrap;
}
}
.singleInvite-tips {
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: white;
}
}
}
</style>
@@ -0,0 +1,125 @@
<template>
<div>
<img class="avatar" :src="callerUserInfo.avatar || defaultAvatar" :onerror="handleErrorImage" />
<div class="nick">{{ callerUserInfo.displayUserInfo }}</div>
<div class="tips">{{ t('Invited group call') }}</div>
<div class="list-tips">{{ t('Those involved') }}</div>
<div class="list">
<div v-for="(item, index) in renderStreamList" :key="index" class="list-item">
<img class="list-avatar" :src="item.avatar || defaultAvatar" :onerror="handleErrorImage" />
<div class="list-name">{{ item.displayUserInfo }}</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from '../../../../../adapter-vue';
import defaultAvatar from '../../../../assets/mobile/default_avatar.png';
import { t } from '../../../../../TUICallService/index';
const props = defineProps({
remoteUsersList: {
type: Array,
required: true,
},
localUserInfo: {
type: Object,
required: true,
},
callerUserInfo:{
type: Object,
required: true,
}
});
function handleErrorImage(e: any) {
e.target.src = defaultAvatar;
}
const renderStreamList = computed(() => {
return [props.localUserInfo, ...props.remoteUsersList];
});
</script>
<style lang="scss" scoped>
.avatar {
width: 120px;
height: 120px;
border-radius: 12px;
display: block;
margin: 40px auto 15px;
}
.tips {
width: 126px;
height: auto;
font-family: PingFangSC-Regular;
font-weight: 400;
font-size: 14px;
color: #333333;
letter-spacing: 0;
margin: 16px auto 40px auto;
text-align: center;
}
.nick {
font-size: 20px;
letter-spacing: 0;
margin: 0 auto;
font-weight: 600;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
}
.list-tips {
width: 112px;
height: auto;
font-family: PingFangSC-Regular;
font-weight: 400;
font-size: 14px;
color: #333333;
letter-spacing: 0;
margin: 0 auto 24px auto;
text-align: center;
}
.list {
position: absolute;
left: 14vw;
margin-top: 0 auto;
display: flex;
flex-wrap: wrap;
width: 272px;
justify-content: center;
flex-wrap: wrap;
}
.list-item {
flex: 0.25;
text-align: center;
width: 64px;
margin: 2px;
.list-avatar {
border-radius: 10%;
width: 64px;
height: 64px;
}
.list-name {
font-family: PingFangSC-Regular;
font-weight: 400;
font-size: 12px;
color: #666666;
letter-spacing: 0;
line-height: 18px;
max-width: 60px;
overflow: hidden;
text-overflow: ellipsis;
line-height: normal;
white-space: nowrap;
}
}
</style>
@@ -0,0 +1,67 @@
<template>
<div class="streamBox">
<RemoteStreamMobile v-if="remoteList.length !== 0 && isMobile && !isWeChat" :local-user-info="localUserInfo" :remote-list="remoteList" :call-status="callStatus"></RemoteStreamMobile>
<RemoteStreamDesk
v-if="remoteList.length !== 0 && !isMobile && !isWeChat"
:local-user-info="localUserInfo"
:remote-list="remoteList"
:call-status="callStatus"
:call-type="callType"
></RemoteStreamDesk>
<RemoteStreamWx
v-if="isMobile && isWeChat"
:local-user-info="localUserInfo"
:remote-list="remoteList"
:call-status="callStatus"
:call-type="callType"
></RemoteStreamWx>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, onUnmounted } from '../../../../adapter-vue';
import RemoteStreamMobile from './mobile/RemoteStreamList.vue';
import RemoteStreamDesk from './deskTop/RemoteStreamList.vue';
import RemoteStreamWx from './wechat/RemoteStreamList.vue';
import { TUIGlobal, TUIStore, StoreName, NAME } from '../../../../TUICallService/index';
const callStatus = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS));
const remoteList = ref(TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST));
const localUserInfo = ref(TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO));
const callType = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE));
const isMobile = !TUIGlobal.isPC;
const isWeChat = TUIGlobal.isWeChat;
const handleLocalUserInfoChange = (value) => {
localUserInfo.value = value;
};
const handleRemoteUserInfoListChange = (value) => {
remoteList.value = value;
};
const handleCallStatusChange = (value) => {
callStatus.value = value;
};
const handleCallMediaTypeChange = (value) => {
callType.value = value;
};
const watchOptions = {
[NAME.LOCAL_USER_INFO]: handleLocalUserInfoChange,
[NAME.REMOTE_USER_INFO_LIST]: handleRemoteUserInfoListChange,
[NAME.CALL_STATUS]: handleCallStatusChange,
[NAME.CALL_MEDIA_TYPE]: handleCallMediaTypeChange,
};
onMounted(() => {
TUIStore.watch(StoreName.CALL, watchOptions, { notifyRangeWhenWatch: NAME.MYSELF });
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CALL, watchOptions);
});
</script>
<style scoped>
.streamBox {
width: 100%;
height: 100%;
}
</style>
@@ -0,0 +1,512 @@
<template>
<div class="calling-wrapper">
<div :class="groupCallingContainerClass">
<div id="localVideo" :class="groupUserViewClass">
<div v-if="callStatus !== 'connected' || callType === CallMediaType.AUDIO" class="user-view-text-container">
<div :class="userViewUserId" class="localStream-userInfo">{{ localUserInfo.displayUserInfo }}</div>
</div>
<span class="tag">
<div class="microphone-icon-container">
<MicVolume v-if="localUserInfo.isAudioAvailable" :volume="localUserInfo.volume"></MicVolume>
<MicVolumeClosed v-else />
</div>
<div class="stream-userInfo"> {{ localUserInfo.displayUserInfo }}{{ t('me') }} </div>
</span>
</div>
<div v-for="remoteUserItem in remoteList" :id="remoteUserItem.domId" :key="remoteUserItem.userId" :class="groupUserViewClass">
<div>
<div v-if="!remoteUserItem.isEnter" class="invite-calling-item-loading">
<img :src="loading" />
</div>
<div v-if="callStatus !== 'connected' || callType === CallMediaType.AUDIO" class="user-view-text-container">
<div class="invite-userInfo" :class="userViewUserId">{{ remoteUserItem.displayUserInfo }}</div>
</div>
</div>
<span class="tag">
<div class="microphone-icon-container">
<MicVolume v-if="remoteUserItem.isAudioAvailable" :volume="remoteUserItem.volume"></MicVolume>
<MicVolumeClosed v-else />
</div>
<div class="stream-userInfo">{{ remoteUserItem.displayUserInfo }}</div>
</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, watchEffect } from '../../../../../adapter-vue';
import MicVolume from '../../../common/MicVolume/MicVolume.vue';
import loading from '../../../../assets/mobile/loading.png';
import MicVolumeClosed from '../../../common/MicVolume/MicVolumeClosed.vue';
import { t, CallMediaType } from '../../../../../TUICallService/index';
const props = defineProps({
remoteList: {
type: Array,
required: true,
},
callStatus: {
type: String,
required: true,
},
callType: {
type: Number,
required: true,
},
localUserInfo: {
type: Object,
required: true,
},
});
const currentPage = ref<number>(1);
const currentPageRemoteList = ref([]);
const groupUserViewClass = ref<string>('group-user-view');
const groupCallingContainerClass = ref<string>('group-calling-container');
const userViewUserId = ref<string>('user-view-user-id');
watch(
currentPageRemoteList,
async () => {
const userViewCount = currentPageRemoteList.value.length + 1;
groupUserViewClass.value = `group-user-view group-user-view-${userViewCount}`;
groupCallingContainerClass.value = `group-calling-container group-calling-container-${userViewCount}`;
userViewUserId.value = `user-view-user-id user-view-user-id-${userViewCount}`;
},
{ flush: 'post', deep: true }
);
watchEffect(async () => {
await refreshCurrentPageRemoteList();
});
async function refreshCurrentPageRemoteList() {
const newPageList: RemoteUser[] = [];
for (let i = (currentPage.value - 1) * 8; i < currentPage.value * 8 && i < props.remoteList.length; i++) {
newPageList.push(props.remoteList[i]);
}
currentPageRemoteList.value = newPageList;
}
</script>
<style lang="scss">
.group-user-view {
> span + div {
background-color: #4c515a !important;
}
}
</style>
<style lang="scss" scoped>
.calling-wrapper {
position: relative;
width: 100%;
height: 100%;
border-radius: inherit;
background: rgba(0, 24, 60, 0.7);
backdrop-filter: blur(34px);
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
z-index: -1;
}
.calling-wrapper:hover {
z-index: inherit;
transition: all 0.5s ease-in-out;
}
#local-dialing,
#localVideo {
background-color: #4c515a;
background-repeat: no-repeat;
background-size: auto;
background-position-x: center; /* 水平居中 */
background-position-y: calc(50% + 10px);
}
.localStream-userInfo {
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
line-height: normal;
white-space: nowrap;
}
.panel-button-area {
display: flex;
flex-direction: row;
//flex-wrap: wrap;
justify-content: center;
margin: 30px 0;
}
.panel-dialing-info {
text-align: center;
}
.control-wrapper:hover {
z-index: 13;
}
.control-item {
display: flex;
flex-direction: column;
width: 100px;
margin: 0 20px;
}
.invite-calling-item-loading {
position: absolute;
left: calc(50% - 20px);
top: calc(50% - 40px);
width: 40px;
height: 40px;
animation: rotation 2s linear infinite;
}
.invite-userInfo {
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
line-height: normal;
white-space: nowrap;
}
@keyframes rotation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.invite-calling-item-loading img {
width: 100%;
height: 100%;
}
.tag {
color: white;
z-index: inherit;
background: rgba(0, 0, 0, 0.6);
padding: 2px 10px;
border-top-right-radius: inherit;
border-bottom-left-radius: inherit;
font-size: 14px;
position: absolute;
left: 0;
bottom: 0;
z-index: 11;
display: flex;
align-items: center;
.microphone-icon-container {
margin-left: 0;
margin-right: 2px;
width: 20px;
background-color: transparent;
}
.stream-userInfo {
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
line-height: normal;
white-space: nowrap;
}
}
.switch-large-small {
position: absolute;
right: 12px;
top: 12px;
width: 16px;
height: 16px;
z-index: inherit;
cursor: pointer;
}
.small-h5 {
position: absolute;
width: 94px;
height: 160px;
min-height: 100px;
max-height: 60%;
top: 30px;
right: 10px;
z-index: 2;
overflow: hidden;
background: #000;
border-radius: 4px;
}
.small {
position: absolute;
width: 260px;
height: 170px;
min-height: 100px;
max-height: 60%;
top: 30px;
left: 30px;
z-index: 2;
overflow: hidden;
resize: both;
background: #000;
border-radius: 4px;
}
.large {
min-height: 100px;
width: 100%;
height: 100%;
z-index: 1;
border-radius: inherit;
overflow: hidden;
}
.large .switch-large-small {
display: none;
}
.group-calling-container {
width: 100%;
height: 100%;
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.group-user-view {
width: 33.333%;
height: 33.333%;
position: relative;
background-color: #4c515a;
backdrop-filter: blur(34px);
color: white;
overflow: hidden;
}
.user-view-text-container {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 11;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
transform: translate(0, -20px);
}
.user-view-user-id {
font-family: 'PingFang SC';
font-style: normal;
font-weight: 600;
font-size: 24px;
line-height: 34px;
}
.user-view-info {
font-family: 'PingFang SC';
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 17px;
}
.page-turn {
width: 40px;
height: 86px;
border-radius: 4.21px;
background: rgba(0, 0, 0, 0.2);
position: absolute;
top: 50%;
transform: translate(0, -50%);
z-index: 11;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.turn-icon-container {
width: 14px;
height: 23px;
}
.right {
right: 0;
}
.microphone-icon-container {
width: 24px;
height: 24px;
background: rgba(0, 0, 0, 0.4);
border-radius: 12px;
display: flex;
justify-content: center;
align-items: center;
margin-left: 8px;
}
.group-user-view-1 {
width: 100%;
height: 100%;
}
.group-calling-container-1 {
height: 100%;
}
.group-user-view-2 {
width: 50%;
height: 100%;
}
.group-calling-container-2 {
height: 50%;
}
.group-user-view-3 {
width: 50%;
height: 50%;
}
.group-user-view-4 {
width: 50%;
height: 50%;
}
.group-calling-container-3 {
height: 100%;
}
.group-calling-container-4 {
height: 100%;
}
.group-user-view-5 {
width: 33.333%;
height: 50%;
}
.group-user-view-6 {
width: 33.333%;
height: 50%;
}
.group-calling-container-5 {
height: 66%;
}
.group-calling-container-6 {
height: 66%;
}
.user-view-user-id-1 {
font-size: 24px;
}
.user-view-user-id-2 {
font-size: 22px;
}
.user-view-user-id-3 {
font-size: 22px;
}
.user-view-user-id-4 {
font-size: 22px;
}
.user-view-user-id-5 {
font-size: 18px;
}
.user-view-user-id-6 {
font-size: 18px;
}
.user-view-user-id-7 {
font-size: 18px;
}
.user-view-user-id-8 {
font-size: 18px;
}
.user-view-user-id-9 {
font-size: 18px;
}
.float-control-panel {
width: 168px;
height: 56px;
background: white;
z-index: 13;
position: fixed;
top: 20px;
right: 20px;
display: flex;
flex-wrap: nowrap;
justify-content: center;
border-radius: 40px;
box-shadow: rgba(0, 0, 0, 0.16) 0px 3px 6px, rgba(0, 0, 0, 0.23) 0px 3px 6px;
}
.function-buttons {
position: absolute;
right: 12px;
top: 12px;
z-index: 13;
cursor: pointer;
display: flex;
flex-direction: row;
justify-content: flex-start;
}
.minimize {
margin-left: 10px;
}
.float-control-item-icon {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 56px;
height: 56px;
position: relative;
}
.float-control-item-icon-container {
border-radius: 40px;
width: 40px;
height: 40px;
margin: 0.5rem;
cursor: pointer;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.float-control-item-icon-container:hover {
background: rgba(218, 218, 218, 0.3);
}
</style>
@@ -0,0 +1,164 @@
<template>
<div class="invite-calling-list">
<Swiper :grid="{ rows: 2, cols: 2 }">
<SwiperSlider v-for="(item, index) in renderStreamList" :key="`${item.userId}`" :index="index" class="invite-calling-item">
<div :id="item.domId" class="invite-calling-item-message">
<div v-if="!item.isEnter && item.domId !== 'localVideo'" class="invite-calling-item-loading">
<img :src="loading" />
</div>
<span v-if="item.isEnter && callStatus === 'connected' || item.domId === 'localVideo'" class="tag">
<div class="microphone-icon-container">
<MicVolume v-if="item.isAudioAvailable" :volume="item.volume"></MicVolume>
<MicVolumeClosed v-else />
</div>
<div class="stream-userInfo"> {{ item.domId === 'localVideo' ? t('me') : item.displayUserInfo }} </div>
</span>
</div>
<img class="avatar" :src="item.avatar || defaultAvatar" :onerror="handleErrorImage" />
<div v-if="!item.isEnter && item.domId !== 'localVideo'" class="invite-calling-item-id">{{ item.nick || item.userId }}</div>
</SwiperSlider>
</Swiper>
</div>
</template>
<script lang="ts" setup>
import { computed } from '../../../../../adapter-vue';
import Swiper from '../../../common/Swiper/Swiper.vue';
import SwiperSlider from '../../../common/Swiper/SwiperSlider.vue';
import loading from '../../../../assets/mobile/loading.png';
import defaultAvatar from '../../../../assets/mobile/default_avatar.png';
import MicVolume from '../../../common/MicVolume/MicVolume.vue';
import MicVolumeClosed from '../../../common/MicVolume/MicVolumeClosed.vue';
import { t } from '../../../../../TUICallService/index';
const props = defineProps({
remoteList: {
type: Array,
required: true,
},
callStatus: {
type: String,
required: true,
},
localUserInfo: {
type: Object,
required: true,
},
});
const renderStreamList = computed(() => {
return [props.localUserInfo, ...props.remoteList];
});
function handleErrorImage(e: any) {
e.target.src = defaultAvatar;
}
</script>
<style lang="scss" scoped>
#localVideo {
width: 100%;
height: 100%;
}
.invite-calling-list {
display: flex;
flex-wrap: wrap;
width: 100%;
justify-content: flex-start;
}
.invite-calling-item {
height: 187px;
background-color: #000;
}
.invite-calling-item image {
width: 100%;
height: 100%;
}
.invite-calling-item-message {
position: absolute;
top: 0;
left: 0;
float: left;
width: 100%;
height: 100%;
z-index: 2;
.tag {
color: white;
z-index: inherit;
background: rgba(0, 0, 0, 0.6);
padding: 2px 10px;
border-top-right-radius: inherit;
border-bottom-left-radius: inherit;
font-size: 14px;
position: absolute;
left: 0;
bottom: 0;
z-index: 11;
display: flex;
align-items: center;
.microphone-icon-container {
margin-top: 4px;
margin-right: 2px;
width: 16px;
background-color: transparent;
}
.stream-userInfo{
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
line-height: normal;
white-space: nowrap;
}
}
}
.invite-calling-item-loading {
position: absolute;
left: calc(50% - 20px);
top: calc(50% - 20px);
width: 40px;
height: 40px;
animation: rotation 2s linear infinite;
}
@keyframes rotation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.invite-calling-item-loading img {
width: 100%;
height: 100%;
}
.invite-calling-item-id {
position: absolute;
left: 2%;
bottom: 2%;
font-family: PingFangSC-Regular;
font-weight: 400;
font-size: 12px;
color: #ffffff;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
line-height: normal;
white-space: nowrap;
}
.avatar {
background-color: black;
width: 100%;
height: 100%;
}
</style>
@@ -0,0 +1,320 @@
<template>
<div class="invite-calling-list">
<SwiperWeChat class="list-box" :render-stream-list="renderStreamList" :grid="{ rows: 2, cols: 2 }">
<SwiperSlider v-for="(item, index) in renderStreamList" :key="`${item.userId}`" :userid="item.userId" :index="index" class="invite-list">
<div class="invite-calling-item">
<div class="invite-calling-item-message" v-if="!item.isEnter || callType === CallMediaType.AUDIO">
<img class="invite-calling-item-loading" v-if="!item.isEnter" :src="loading" />
<img class="avatar" :src="item.avatar || defaultAvatar" :onerror="handleErrorImage" />
<div class="invite-calling-item-id">{{ item.displayUserInfo }}</div>
</div>
<div :class="callType === CallMediaType.AUDIO ? 'invite-calling-audio' : 'invite-calling-item-stream'" v-if="item.isEnter">
<div class="pusher-box" v-if="item.userId === localUserInfo.userId">
<live-pusher
class="pusher"
:url="pusher.url"
:mode="pusher.mode"
:autopush="true"
:enable-camera="pusher.enableCamera"
:enable-mic="true"
:muted="!pusher.enableMic"
:enable-agc="true"
:enable-ans="true"
:enable-ear-monitor="pusher.enableEarMonitor"
auto-focus="false"
:zoom="pusher.enableZoom"
:min-bitrate="pusher.minBitrate"
:max-bitrate="pusher.maxBitrate"
:video-width="pusher.videoWidth"
:video-height="pusher.videoHeight"
:beauty="pusher.beautyLevel"
:whiteness="pusher.whitenessLevel"
:orientation="pusher.videoOrientation"
:aspect="pusher.videoAspect"
:device-position="pusher.frontCamera"
:remote-mirror="pusher.enableRemoteMirror"
:local-mirror="pusher.localMirror"
:background-mute="pusher.enableBackgroundMute"
:audio-quality="pusher.audioQuality"
:audio-volume-type="pusher.audioVolumeType"
:audio-reverb-type="pusher.audioReverbType"
:waiting-img="pusher.waitingImage"
:beauty-style="pusher.beautyStyle"
:filter="pusher.filter"
@statechange="pusherStateChangeHandler"
@netstatus="pusherNetStatus"
@error="pusherErrorHandler"
@audiovolumenotify="pusherAudioVolumeNotify"
/>
<div v-if='callType !== CallMediaType.AUDIO' class="invite-calling-item-id">{{ item.nick || item.userId }}</div>
</div>
<div class="player-container" v-else>
<live-player
class="player"
v-if="playerProcess[item.userId] && playerProcess[item.userId].userID === item.userId && (playerProcess[item.userId].hasAudio || playerProcess[item.userId].hasVideo)"
:id="playerProcess[item.userId].id"
:data-userid="playerProcess[item.userId].userID"
:data-streamid="playerProcess[item.userId].streamID"
:data-streamtype="playerProcess[item.userId].streamType"
:src="playerProcess[item.userId].src"
mode="RTC"
:autoplay="playerProcess[item.userId].autoplay"
:mute-audio="playerProcess[item.userId].muteAudio"
:mute-video="playerProcess[item.userId].muteVideo"
:orientation="playerProcess[item.userId].orientation"
:object-fit="playerProcess[item.userId].objectFit"
:background-mute="playerProcess[item.userId].enableBackgroundMute"
:min-cache="playerProcess[item.userId].minCache"
:max-cache="playerProcess[item.userId].maxCache"
:sound-mode="soundMode"
:enable-recv-message="playerProcess[item.userId].enableRecvMessage"
:auto-pause-if-navigate="playerProcess[item.userId].autoPauseIfNavigate"
:auto-pause-if-open-native="playerProcess[item.userId].autoPauseIfOpenNative"
@statechange="playerStateChange"
@fullscreenchange="playerFullscreenChange"
@netstatus="playNetStatus"
@audiovolumenotify="playerAudioVolumeNotify"
/>
<div v-if='callType !== CallMediaType.AUDIO' class="invite-calling-item-id">{{ item.displayUserInfo }}</div>
</div>
</div>
</div>
</SwiperSlider>
</SwiperWeChat>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref } from '../../../../../adapter-vue';
import SwiperWeChat from '../../../common/SwiperWx/Swiper.vue';
import SwiperSlider from '../../../common/SwiperWx/SwiperSlider.vue';
import { TUICallKitServer, StoreName, NAME, TUIStore, CallMediaType, AudioPlayBackDevice } from '../../../../../TUICallService/index';
import loading from '../../../../assets/mobile/loading.png';
import defaultAvatar from '../../../../assets/mobile/default_avatar.png';
const pusher = ref(TUIStore.getData(StoreName.CALL, NAME.PUSHER));
const playerList = ref(TUIStore.getData(StoreName.CALL, NAME.PLAYER));
const soundMode = ref(TUIStore.getData(StoreName.CALL, NAME.IS_EAR_PHONE) ? AudioPlayBackDevice.EAR : AudioPlayBackDevice.SPEAKER);
const playerProcess = ref({});
const props = defineProps({
remoteList: {
type: Array,
required: true,
},
callStatus: {
type: String,
required: true,
},
localUserInfo: {
type: Object,
required: true,
},
callType: {
type: Number,
required: true,
},
});
const handlePusherChange = (value) => {
Object.assign(pusher.value, value);
};
const handlePlayerChange = (value) => {
Object.assign(playerList.value, value);
playerProcess.value = convertToObj(playerList.value);
};
const handEarPhoneChange = (value) => {
soundMode.value = value ? AudioPlayBackDevice.EAR : AudioPlayBackDevice.SPEAKER;
};
const watchOptions = {
[NAME.PUSHER]: handlePusherChange,
[NAME.PLAYER]: handlePlayerChange,
[NAME.IS_EAR_PHONE]: handEarPhoneChange,
};
onMounted(() => {
TUIStore.watch(StoreName.CALL, watchOptions, { notifyRangeWhenWatch: NAME.MYSELF });
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CALL, watchOptions);
});
const renderStreamList = computed(() => {
return [props.localUserInfo, ...props.remoteList];
});
function convertToObj(arr = []) {
const tempObject = {};
for (let i = 0; i < arr.length; i++) {
tempObject[arr[i].userID] = arr[i];
}
return tempObject;
}
function handleErrorImage(e: any) {
e.target.src = defaultAvatar;
}
function pusherStateChangeHandler(e: any) {
TUICallKitServer._tuiCallEngine._pusherStateChangeHandler(e);
}
function pusherNetStatus(e: any) {
TUICallKitServer._tuiCallEngine._pusherNetStatus(e);
}
function pusherErrorHandler(e: any) {
TUICallKitServer._tuiCallEngine._pusherNetStatus(e);
}
function pusherAudioVolumeNotify(e: any) {
TUICallKitServer._tuiCallEngine._pusherAudioVolumeNotify(e);
}
function playerStateChange(e: any) {
TUICallKitServer._tuiCallEngine._playerStateChange(e);
}
function playerFullscreenChange(e: any) {
// TUICallKitServer._tuiCallEngine._playNetStatus(e);
}
function playNetStatus(e: any) {
TUICallKitServer._tuiCallEngine._playNetStatus(e);
}
function playerAudioVolumeNotify(e: any) {
TUICallKitServer._tuiCallEngine._playerAudioVolumeNotify(e);
}
</script>
<style lang="scss" scoped>
.invite-list {
width: 100%;
height: 187px;
}
.invite-calling-list {
display: flex;
flex-wrap: wrap;
width: 100%;
justify-content: flex-start;
.list-box {
display: flex;
flex-wrap: wrap;
width: 100%;
justify-content: flex-start;
.invite-calling-item {
width: 100%;
height: 187px;
.invite-calling-item-message {
width: 100%;
height: 100%;
position: relative;
.invite-calling-item-loading {
position: absolute;
left: calc(50% - 20px);
top: calc(50% - 20px);
width: 40px;
height: 40px;
animation: rotation 2s linear infinite;
}
@keyframes rotation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.avatar {
width: 100%;
height: 100%;
}
.invite-calling-item-id {
position: absolute;
left: 2%;
bottom: 2%;
font-family: PingFangSC-Regular;
font-weight: 400;
font-size: 12px;
color: #ffffff;
}
}
.invite-calling-audio {
width: 0px;
height: 0px;
.pusher-box {
width: 0px;
height: 0px;
.pusher {
width: 0px;
height: 0px;
}
}
.player-container {
width: 0px;
height: 0px;
.player {
width: 0px;
height: 0px;
}
}
}
.invite-calling-item-stream {
width: 100%;
height: 100%;
.pusher-box {
width: 100%;
height: 100%;
.pusher {
width: 100%;
height: 100%;
}
.invite-calling-item-id {
position: absolute;
left: 2%;
bottom: 2%;
font-family: PingFangSC-Regular;
font-weight: 400;
font-size: 12px;
color: #ffffff;
}
}
.player-container {
width: 100%;
height: 100%;
background-color: black;
.player {
width: 100%;
height: 100%;
}
.invite-calling-item-id {
position: absolute;
left: 2%;
bottom: 2%;
font-family: PingFangSC-Regular;
font-weight: 400;
font-size: 12px;
color: #ffffff;
}
}
}
}
}
}
</style>
@@ -0,0 +1,129 @@
<template>
<div>
<!-- 移动端呼叫状态 -->
<div v-if="callStatus === CallStatus.CALLING && isMobile" :class="['control-wrapper-mobile', textColor]">
<div v-if="callType === CallMediaType.VIDEO && isSponsor && !isH5" class="btn-operate">
<SwitchAudioCall class="mobile-switchAudioCall" :is-mobile="isMobile"></SwitchAudioCall>
</div>
<div class="btn-operate">
<Reject v-if="!isSponsor" class="mobile-reject" :is-mobile="isMobile"></Reject>
<Hangup v-if="isSponsor" :isGroupCall="isGroupCall" :callType="callType" :is-mobile="isMobile" :isClickable="isClickable"></Hangup>
<SwitchCamera v-if="isSponsor && callType === CallMediaType.VIDEO && !isH5" class="mobile-switchcamera"></SwitchCamera>
<Accept v-if="!isSponsor" class="mobile-accept" :is-mobile="isMobile" :call-type="callType"> </Accept>
</div>
</div>
<!-- 移动端通话状态 -->
<div v-if="callStatus === CallStatus.CONNECTED && isMobile" :class="['control-wrapper-mobile', textColor]">
<div class="btn-operate">
<Timer :call-duration="callDuration"></Timer>
</div>
<div v-if="callType === CallMediaType.VIDEO" class="btn-operate">
<SwitchAudioCall class="mobile-switchAudioCall" :is-mobile="isMobile"></SwitchAudioCall>
</div>
<div v-if="callType === CallMediaType.VIDEO" class="btn-operate">
<Microphone class="btn" :is-mobile="isMobile"></Microphone>
<Speaker class="btn" v-if="!isH5"></Speaker>
<CameraBox class="btn" :is-mobile="isMobile"></CameraBox>
</div>
<div class="btn-operate">
<Microphone class="btn" v-if="callType === CallMediaType.AUDIO" :is-mobile="isMobile"></Microphone>
<Hangup class="mobile-hangup btn" :is-mobile="isMobile" :isGroupCall="isGroupCall" :callType="callType" :isClickable="isClickable"></Hangup>
<SwitchCamera class="btn mobile-switchcamera" v-if="callType === CallMediaType.VIDEO && !isH5"></SwitchCamera>
<Speaker class="btn" v-if="callType === CallMediaType.AUDIO && !isH5"></Speaker>
</div>
</div>
<!-- 桌面端呼叫状态 -->
<div v-if="callStatus === CallStatus.CALLING && !isMobile" class="control-wrapper-desktop">
<div class="panel-button-area">
<CameraBox class="btn" v-if="callType === CallMediaType.VIDEO" :is-mobile="isMobile"></CameraBox>
<Microphone class="btn" v-if="isSponsor" :is-mobile="isMobile"></Microphone>
<Reject class="btn" v-if="!isSponsor" :is-mobile="isMobile"></Reject>
<Accept class="btn" v-if="!isSponsor" :is-mobile="isMobile" :call-type="callType"></Accept>
<Hangup class="btn" v-if="isSponsor" :isGroupCall="isGroupCall" :callType="callType" :is-mobile="isMobile" :isClickable="isClickable"></Hangup>
</div>
</div>
<!-- 桌面端通话状态 -->
<div v-if="callStatus === CallStatus.CONNECTED && !isMobile" class="control-wrapper-desktop">
<div class="panel-button-area">
<CameraBox class="btn" v-if="callType === CallMediaType.VIDEO" :is-mobile="isMobile"></CameraBox>
<Microphone class="btn" :is-mobile="isMobile"></Microphone>
<SwitchAudioCall class="btn" v-if="callType === CallMediaType.VIDEO" :is-mobile="isMobile"></SwitchAudioCall>
<Hangup class="btn" :is-mobile="isMobile" :isGroupCall="isGroupCall" :callType="callType" :isClickable="isClickable"></Hangup>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from '../../../../adapter-vue';
import Hangup from '../../common/Button/Hangup.vue';
import SwitchCamera from '../../common/Button/SwitchCamera.vue';
import Accept from '../../common/Button/Accept.vue';
import Reject from '../../common/Button/Reject.vue';
import SwitchAudioCall from '../../common/Button/SwitchAudioCall.vue';
import Microphone from '../../common/Button/Microphone.vue';
import Speaker from '../../common/Button/Speaker.vue';
import CameraBox from '../../common/Button/Camera.vue';
import Timer from '../../common/Button/Timer.vue';
import { TUIGlobal, TUIStore, StoreName, NAME, CallMediaType, CallStatus, CallRole } from '../../../../TUICallService/index';
const callRole = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE));
const callType = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE));
const callStatus = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS));
const isGroupCall = ref(TUIStore.getData(StoreName.CALL, NAME.IS_GROUP));
const callDuration = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_DURATION));
const isClickable = ref(TUIStore.getData(StoreName.CALL, NAME.IS_CLICKABLE));
const isMobile = !TUIGlobal.isPC;
const isH5 = TUIGlobal.isH5;
let isSponsor = callRole.value === CallRole.CALLER;
const textColor = ref(callType.value === CallMediaType.AUDIO ? 'black' : 'white');
const handleCallRoleChange = (value) => {
isSponsor = value === CallRole.CALLER;
callRole.value = value;
};
const handleIsGroupChange = (value) => {
isGroupCall.value = value;
};
const handleCallMediaTypeChange = (value) => {
callType.value = value;
textColor.value = callType.value === CallMediaType.AUDIO ? 'black' : 'white';
};
const handleCallStatusChange = (value) => {
callStatus.value = value;
};
const handleCallDurationChange = (value) => {
callDuration.value = value;
};
const handleIsClickableChange = (value) => {
isClickable.value = value;
};
onMounted(() => {
TUIStore.watch(StoreName.CALL, {
[NAME.CALL_ROLE]: handleCallRoleChange,
[NAME.IS_GROUP]: handleIsGroupChange,
[NAME.CALL_MEDIA_TYPE]: handleCallMediaTypeChange,
[NAME.CALL_STATUS]: handleCallStatusChange,
[NAME.CALL_DURATION]: handleCallDurationChange,
[NAME.IS_CLICKABLE]: handleIsClickableChange,
}, {
notifyRangeWhenWatch: NAME.MYSELF,
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CALL, {
[NAME.CALL_ROLE]: handleCallRoleChange,
[NAME.IS_GROUP]: handleIsGroupChange,
[NAME.CALL_MEDIA_TYPE]: handleCallMediaTypeChange,
[NAME.CALL_STATUS]: handleCallStatusChange,
[NAME.CALL_DURATION]: handleCallDurationChange,
[NAME.IS_CLICKABLE]: handleIsClickableChange,
});
});
</script>
<style lang="scss" src="./style/index.scss" scoped></style>
@@ -0,0 +1,64 @@
//桌面端
.control-wrapper-desktop {
transform: translate(-50%);
position: absolute;
bottom: 0;
padding-bottom: 40px;
left: 50%;
width: 100%;
color: white;
border-radius: inherit;
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.4) 100%
);
display: flex;
flex-direction: column;
flex-wrap: wrap;
align-items: center;
user-select: none;
.panel-button-area {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-evenly;
margin: 30px 0;
min-width: 40%;
.btn {
margin: 0 35px;
}
.btn-operate-desktop {
.call-operate {
display: flex;
flex-direction: column;
width: 40px;
height: 40px;
margin: 0 40px;
cursor: pointer;
}
img {
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
position: relative;
}
span {
display: block;
margin-top: 5px;
white-space: nowrap;
font-weight: 400;
font-size: 14px;
line-height: 20px;
text-align: center;
}
}
}
}
@@ -0,0 +1,2 @@
@import './desktop.scss';
@import './mobile.scss';
@@ -0,0 +1,50 @@
.control-wrapper-mobile {
position: absolute;
bottom: 20px;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.btn-operate {
display: flex;
justify-content: center;
align-items: center;
position: relative;
font-size: 16px;
letter-spacing: 0;
font-weight: 400;
margin-bottom: 6px;
.mobile-reject{
margin: 0px 16vw 40px 16vw;
}
.mobile-accept{
margin: 0px 16vw 40px 16vw;
}
.mobile-switchAudioCall{
font-size: 14px;
}
.mobile-switchcamera {
position: absolute;
left: 80px;
top: 10px;
}
.btn {
margin: 0 5vw;
}
}
}
.black{
color: black;
}
.white{
color: white;
}
@@ -0,0 +1,165 @@
<template>
<div>
<!-- 移动端 -->
<div v-if="!isGroupCall && isMobile" class="singleInvite-header-mobile">
<!-- 音频通话 -->
<SingleAudio
v-if="callType === CallMediaType.AUDIO && remoteUsersList.length !== 0"
class="singleAudio"
:remote-users-list="remoteUsersList"
:is-sponsor="isSponsor"
:call-alert-message="callAlertMessage"
:call-status="callStatus"
>
</SingleAudio>
<!-- 视频通话 -->
<SingleVideo
v-if="callType === CallMediaType.VIDEO && remoteUsersList.length !== 0"
class="singleVideo"
:remote-users-list="remoteUsersList"
:is-sponsor="isSponsor"
:call-alert-message="callAlertMessage"
>
</SingleVideo>
</div>
<!-- 桌面端 -->
<div v-if="!isGroupCall && !isMobile && remoteUsersList.length !== 0" class="singleInvite-header-desktop">
<div class="singleInvite-message">
<div class="singleInvite-nick">
<div class="singleInvite-nick-span"> {{ remoteUsersList[0].displayUserInfo }} </div>
<div v-if="callType === CallMediaType.AUDIO && callStatus === 'connected'" class="microphone-icon-container">
<MicVolume v-if="remoteUsersList[0].isAudioAvailable" :volume="remoteUsersList[0].volume"></MicVolume>
<MicVolumeClosed v-else />
</div>
</div>
<span v-if="callStatus !== CallStatus.CONNECTED" class="singleInvite-tips">{{ callAlertMessage }}</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from '../../../../adapter-vue';
import SingleAudio from './mobile/SingleAudio.vue';
import SingleVideo from './mobile/SingleVideo.vue';
import MicVolume from '../../common/MicVolume/MicVolume.vue';
import MicVolumeClosed from '../../common/MicVolume/MicVolumeClosed.vue';
import { TUIGlobal, TUIStore, StoreName, NAME, CallMediaType, CallRole, CallStatus } from '../../../../TUICallService/index';
const { CALL_ROLE, CALL_MEDIA_TYPE, IS_GROUP, REMOTE_USER_INFO_LIST, CALL_STATUS, CALL_TIPS } = NAME;
const callRole = ref<string>(TUIStore.getData(StoreName.CALL, CALL_ROLE));
const callType = ref<number>(TUIStore.getData(StoreName.CALL, CALL_MEDIA_TYPE));
const isGroupCall = ref<boolean>(TUIStore.getData(StoreName.CALL, IS_GROUP));
const remoteUsersList = ref(TUIStore.getData(StoreName.CALL, REMOTE_USER_INFO_LIST));
const callStatus = ref(TUIStore.getData(StoreName.CALL, CALL_STATUS));
const callAlertMessage = ref(TUIStore.getData(StoreName.CALL, CALL_TIPS));
const isMobile = !TUIGlobal.isPC;
const isSponsor = callRole.value === CallRole.CALLER;
const handleCallRoleChange = (value) => {
callRole.value = value;
};
const handleIsGroupChange = (value) => {
isGroupCall.value = value;
};
const handleCallMediaTypeChange = (value) => {
callType.value = value;
};
const handleRemoteUserInfoListChange = (value) => {
remoteUsersList.value = value;
};
const handleCallStatusChange = (value) => {
callStatus.value = value;
};
const handleCallTipsChange = (value) => {
callAlertMessage.value = value;
};
onMounted(() => {
TUIStore.watch(StoreName.CALL, {
[CALL_ROLE]: handleCallRoleChange,
[IS_GROUP]: handleIsGroupChange,
[CALL_MEDIA_TYPE]: handleCallMediaTypeChange,
[REMOTE_USER_INFO_LIST]: handleRemoteUserInfoListChange,
[CALL_STATUS]: handleCallStatusChange,
[CALL_TIPS]: handleCallTipsChange,
}, {
notifyRangeWhenWatch: NAME.MYSELF,
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CALL, {
[CALL_STATUS]: handleCallStatusChange,
[CALL_MEDIA_TYPE]: handleCallMediaTypeChange,
[CALL_ROLE]: handleCallRoleChange,
[IS_GROUP]: handleIsGroupChange,
[REMOTE_USER_INFO_LIST]: handleRemoteUserInfoListChange,
[CALL_TIPS]: handleCallTipsChange,
});
});
</script>
<style lang="scss">
.singleInvite-header-mobile {
display: flex;
justify-content: flex-end;
padding: 0 26px;
.singleAudio {
margin: 0 auto;
}
.singleVideo {
display: flex;
}
}
.singleInvite-header-desktop {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
position: relative;
top: 30%;
.singleInvite-message .singleInvite-nick {
font-style: normal;
font-weight: 600;
font-size: 40px;
line-height: 56px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100%;
color: white;
.singleInvite-nick-span {
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
line-height: normal;
white-space: nowrap;
}
.microphone-icon-container {
width: 24px;
height: 24px;
background: rgba(0, 0, 0, 0.4);
border-radius: 12px;
line-height: 24px;
margin-left: 8px;
}
}
.singleInvite-tips {
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: white;
}
}
</style>
@@ -0,0 +1,74 @@
<template>
<div class="singleInvite-message">
<img class="singleInvite-avatar" :src="remoteUsersList[0].avatar || defaultAvatar" :onerror="handleErrorImage" />
<div class="singleInvite-nick">{{ remoteUsersList[0].displayUserInfo }}</div>
<span v-if="callStatus === CallStatus.CALLING" class="singleInvite-tips">{{ callAlertMessage }}</span>
</div>
</template>
<script lang="ts" setup>
import defaultAvatar from '../../../../assets/mobile/default_avatar.png';
import { CallStatus } from '../../../../../TUICallService/index';
const props = defineProps({
isSponsor: {
type: Boolean,
required: true,
},
remoteUsersList: {
type: Array,
required: true,
},
callAlertMessage: {
type: String,
required: true,
},
callStatus: {
type: String,
required: true,
},
});
function handleErrorImage(e: any) {
e.target.src = defaultAvatar;
}
</script>
<style lang="scss" scoped>
.singleInvite-message {
display: flex;
flex-direction: column;
padding: 0 16px;
justify-content: center;
align-items: center;
.singleInvite-nick {
line-height: 40px;
text-align: center;
font-size: 20px;
color: #333333;
letter-spacing: 0;
font-weight: 500;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.singleInvite-tips {
height: 20px;
font-size: 14px;
color: #97989c;
letter-spacing: 0;
font-weight: 400;
text-align: center;
}
}
.singleInvite-avatar {
width: 100px;
height: 100px;
border-radius: 12px;
margin-bottom: 12px;
margin: 0 auto;
}
</style>
@@ -0,0 +1,67 @@
<template>
<div class="singleInvite-box">
<div class="singleInvite-message">
<div class="singleInvite-nick">{{ remoteUsersList[0].displayUserInfo }}</div>
<span class="singleInvite-tips">{{ callAlertMessage }}</span>
</div>
<img class="singleInvite-avatar" :src="remoteUsersList[0].avatar || defaultAvatar" :onerror="handleErrorImage" />
</div>
</template>
<script lang="ts" setup>
import defaultAvatar from '../../../../assets/mobile/default_avatar.png';
const props = defineProps({
isSponsor: {
type: Boolean,
required: true,
},
remoteUsersList: {
type: Array,
required: true,
},
callAlertMessage: {
type: String,
required: true,
},
});
function handleErrorImage(e: any) {
e.target.src = defaultAvatar;
}
</script>
<style lang="scss" scoped>
.singleInvite-box {
display: flex;
.singleInvite-message {
color: #ffffff;
display: flex;
flex-direction: column;
padding: 0 16px;
justify-content: center;
.singleInvite-nick {
font-size: 20px;
letter-spacing: 0;
margin: 0 auto;
font-weight: 600;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.singleInvite-tips {
font-size: 14px;
letter-spacing: 0;
text-align: center;
font-weight: 400;
}
}
.singleInvite-avatar {
width: 100px;
height: 100px;
border-radius: 12px;
}
}
</style>
@@ -0,0 +1,255 @@
<template>
<div class="streamBox">
<!-- 移动端 -->
<div v-show="callType === CallMediaType.VIDEO" v-if="isMobile" class="streamBox-mobile" @click="toggleMinimize">
<!-- 本地流 -->
<div id="localVideo" :class="localClass" @click="switchScreen(currentLocalScreen ? remoteList[0].domId:'localVideo',localClass)"></div>
<!-- 远端流 -->
<div v-if="remoteList.length !== 0" :id="remoteList[0].domId" :class="remoteClass" @click="switchScreen(currentLocalScreen ? remoteList[0].domId:'localVideo',remoteClass)"></div>
<div v-if="isShowFloatWindow" class="callDuration-text">{{ callStatus !== CallStatus.CONNECTED ? t('waiting') : callDuration }}</div>
</div>
<!-- 桌面端 -->
<div v-show="callType === CallMediaType.VIDEO" v-if="!isMobile" class="streamBox-deskTop" :class="localUserInfo.isVideoAvailable ? 'streamDisplay' : '' ">
<div id="localVideo" :class="localClass">
<span v-if="callStatus === CallStatus.CONNECTED" class="tag">
<div class="microphone-icon-container">
<MicVolume v-if="localUserInfo.isAudioAvailable" :volume="localUserInfo.volume"></MicVolume>
<MicVolumeClosed v-else />
</div>
<div class="stream-userInfo">{{`${localUserInfo.displayUserInfo} ${t('me')}` }}</div>
</span>
</div>
<div v-if="showRemoteStream" :class="remoteClass">
<div :id="remoteList[0].domId" class="remoteListBox">
<span class="tag">
<div class="microphone-icon-container">
<MicVolume v-if="remoteList[0].isAudioAvailable" :volume="remoteList[0].volume"></MicVolume>
<MicVolumeClosed v-else />
</div>
<div class="stream-userInfo">{{ remoteList[0].displayUserInfo }}</div>
</span>
</div>
</div>
<div v-if="showSwitchScreen" class="switchScreenBox" @click="switchScreen(currentLocalScreen ? remoteList[0].domId:'localVideo')">
<div class="screenImg">
<img :src="switchScreenImg" />
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, onMounted, onUnmounted, computed } from '../../../../adapter-vue';
import { TUIGlobal, TUIStore, StoreName, NAME, t, CallMediaType, CallStatus, TUICallKitServer } from '../../../../TUICallService/index';
import switchScreenImg from '../../../assets/desktop/switchScreen.svg';
import MicVolume from '../../common/MicVolume/MicVolume.vue';
import MicVolumeClosed from '../../common/MicVolume/MicVolumeClosed.vue';
const callType = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE));
const callStatus = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS));
const localClass = ref('largeView');
const remoteClass = ref('smallView');
const currentLocalScreen = ref(true);
const remoteList = ref(TUIStore.getData(StoreName.CALL, NAME.REMOTE_USER_INFO_LIST));
const localUserInfo = ref(TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO));
const callDuration = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_DURATION));
const isSwitchScreen = ref<Boolean>(false);
const isShowFloatWindow = ref<Boolean>(false);
const showRemoteStream = ref<Boolean>(false)
const isMobile = !TUIGlobal.isPC;
const showSwitchScreen = computed(() => {
return callStatus.value === CallStatus.CONNECTED && remoteList.value.length !== 0 && callType.value === CallMediaType.VIDEO;
});
const handleLocalUserInfoChange = (value) => {
localUserInfo.value = value;
};
const handleRemoteUserInfoListChange = (value) => {
showRemoteStream.value = value[0]?.isEnter;
remoteList.value = value;
};
const handleCallStatusChange = (value) => {
callStatus.value = value;
};
const handleCallMediaTypeChange = (value) => {
callType.value = value;
};
const handleIsShowFloatWindowChange = (value) => {
isShowFloatWindow.value = value;
};
const toggleMinimize = () => {
if (callType.value === CallMediaType.AUDIO || !isShowFloatWindow.value) return;
TUICallKitServer.toggleMinimize();
};
const handleCallDurationChange = (value) => {
callDuration.value = value;
};
const handleScreenChange = (value) => {
if (!value) return;
if (value === 'localVideo') {
localClass.value = 'largeView';
remoteClass.value = 'smallView';
currentLocalScreen.value = true;
} else {
remoteClass.value = 'largeView';
localClass.value = 'smallView';
currentLocalScreen.value = false;
}
};
const watchOptions = {
[NAME.IS_MINIMIZED]: handleIsShowFloatWindowChange,
[NAME.LOCAL_USER_INFO]: handleLocalUserInfoChange,
[NAME.REMOTE_USER_INFO_LIST]: handleRemoteUserInfoListChange,
[NAME.CALL_STATUS]: handleCallStatusChange,
[NAME.CALL_MEDIA_TYPE]: handleCallMediaTypeChange,
[NAME.CALL_DURATION]: handleCallDurationChange,
[NAME.BIG_SCREEN_USER_ID]: handleScreenChange,
};
onMounted(() => {
TUIStore.watch(StoreName.CALL, watchOptions, {
notifyRangeWhenWatch: NAME.MYSELF,
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CALL, watchOptions);
});
const switchScreen = (value,className) => {
if( callStatus.value !== CallStatus.CONNECTED || (isMobile && className === 'largeView')) return;
TUICallKitServer.switchScreen(value);
};
</script>
<style lang="scss">
.streamDisplay {
.remoteListBox, .remoteList-desk, .localVideo {
> div {
background-color: #4c515a !important;
}
}
}
</style>
<style lang="scss" scoped>
.streamBox {
width: 100%;
height: 100%;
.callDuration-text {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
font-size: 12px;
color: #ffffff;
font-family: PingFang SC;
font-weight: 500px;
line-height: 20px;
}
}
.streamBox-mobile {
position: relative;
height: 100%;
.smallView {
position: fixed;
top: 8%;
right: 10%;
z-index: 999;
width: 40%;
height: 30%;
.remoteListBox {
width: 40%;
height: 30%;
}
}
.largeView {
width: 100%;
height: 100%;
}
}
.streamBox-deskTop {
position: relative;
width: 100%;
height: 100%;
.switchScreenBox {
cursor: pointer;
position: absolute;
top: 20px;
left: 260px;
z-index: 999;
.screenImg {
width: 16px;
}
}
.largeView {
width: 100%;
height: 100%;
.remoteListBox{
width: 100%;
height: 100%;
}
}
.smallView {
position: absolute;
top: 20px;
left: 20px;
width: 260px;
height: 180px;
z-index: 999;
background-color: black;
.remoteListBox {
width: 100%;
height: 100%;
}
}
.tag {
color: white;
z-index: inherit;
background: rgba(0, 0, 0, 0.6);
padding: 2px 10px;
border-top-right-radius: inherit;
border-bottom-left-radius: inherit;
font-size: 14px;
position: absolute;
left: 0;
bottom: 0;
z-index: 11;
display: flex;
align-items: center;
.microphone-icon-container {
margin-left: 0;
margin-right: 2px;
width: 20px;
background-color: transparent;
}
.stream-userInfo {
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
line-height: normal;
white-space: nowrap;
}
}
}
</style>
@@ -0,0 +1,103 @@
<template>
<!-- 移动端 -->
<div v-show="callType === CallMediaType.VIDEO" @click="toggleMinimize" class="streamBox-mobile">
<!-- 本地流 -->
<Pusher :localClass="localClass" :currentLocalScreen="currentLocalScreen"></Pusher>
<!-- 远端流 -->
<Player v-if="callStatus === CallStatus.CONNECTED" :remoteClass="remoteClass" :currentLocalScreen="currentLocalScreen"></Player>
<div v-if="isShowFloatWindow" class="callDuration-text">{{callStatus !== CallStatus.CONNECTED ? t('waiting') :callDuration}}</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from '../../../../adapter-vue';
import { TUICallKitServer, TUIStore, StoreName, NAME, CallMediaType, CallStatus, t } from '../../../../TUICallService/index';
import Pusher from './wechat/Pusher.vue';
import Player from './wechat/Player.vue';
const localClass = ref('large-view');
const remoteClass = ref('small-view');
const currentLocalScreen = ref(true);
const callType = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE));
const callStatus = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS));
const callDuration = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_DURATION));
const isShowFloatWindow = ref(false);
const handleCallMediaTypeChange = (value) => {
callType.value = value;
};
const handleIsShowFloatWindowChange = (value) => {
isShowFloatWindow.value = value;
};
const toggleMinimize = () =>{
if( callType.value === CallMediaType.AUDIO || !isShowFloatWindow.value ) return;
TUICallKitServer.toggleMinimize();
}
const handleCallStatusChange = (value) => {
callStatus.value = value;
};
const handleCallDurationChange = (value) => {
callDuration.value = value;
};
const handleScreenChange = (value) => {
if (!value) return;
if (value === 'localVideo') {
localClass.value = 'large-view';
remoteClass.value = 'small-view';
currentLocalScreen.value = true;
} else {
remoteClass.value = 'large-view';
localClass.value = 'small-view';
currentLocalScreen.value = false;
}
};
const watchOptions = {
[NAME.CALL_MEDIA_TYPE]: handleCallMediaTypeChange,
[NAME.IS_MINIMIZED]: handleIsShowFloatWindowChange,
[NAME.CALL_STATUS]: handleCallStatusChange,
[NAME.CALL_DURATION]: handleCallDurationChange,
[NAME.BIG_SCREEN_USER_ID]: handleScreenChange,
};
onMounted(function () {
TUIStore.watch(
StoreName.CALL,watchOptions,
{
notifyRangeWhenWatch: NAME.MYSELF,
},
);
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CALL, watchOptions);
});
</script>
<style lang="scss" scoped>
.streamBox-mobile {
width: 100%;
height: 100%;
position: relative;
background-color: black;
.callDuration-text{
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
font-size: 12px;
color: #FFFFFF;
font-family: PingFang SC;
font-weight: 500px;
line-height: 20px;
}
}
</style>
@@ -0,0 +1,99 @@
<template>
<div :class="remoteClass">
<div v-for="(item, index) in playerList" :key="index" @click="switchScreen" class="stream-box">
<live-player
v-if="item.hasAudio || item.hasVideo"
class="stream"
:id="item.id"
:data-userid="item.userID"
:data-streamid="item.streamID"
:data-streamtype="item.streamType"
:src="item.src"
mode="RTC"
:autoplay="item.autoplay"
:mute-audio="item.muteAudio"
:mute-video="item.muteVideo"
:orientation="item.orientation"
:object-fit="item.objectFit"
:background-mute="item.enableBackgroundMute"
:min-cache="item.minCache"
:max-cache="item.maxCache"
:sound-mode="soundMode"
:enable-recv-message="item.enableRecvMessage"
:auto-pause-if-navigate="item.autoPauseIfNavigate"
:auto-pause-if-open-native="item.autoPauseIfOpenNative"
@statechange="playerStateChange"
@fullscreenchange="playerFullscreenChange"
@netstatus="playNetStatus"
@audiovolumenotify="playerAudioVolumeNotify"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { TUICallKitServer, StoreName, NAME, TUIStore, CallStatus, AudioPlayBackDevice } from '../../../../../TUICallService/index';
import { onMounted, ref, onUnmounted } from '../../../../../adapter-vue';
const playerList = ref(TUIStore.getData(StoreName.CALL, NAME.PLAYER));
const callStatus = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS));
const soundMode = ref(TUIStore.getData(StoreName.CALL, NAME.IS_EAR_PHONE) ? AudioPlayBackDevice.EAR : AudioPlayBackDevice.SPEAKER);
const props = defineProps({
remoteClass: {
type: String,
required: true,
},
currentLocalScreen: {
type: Boolean,
required: true,
},
});
const handlePlayerListChange = (value) => {
playerList.value = Object.assign({}, playerList.value, value);
};
const handleCallStatusChange = (value) => {
callStatus.value = value;
};
const handEarPhoneChange = (value) => {
soundMode.value = value ? AudioPlayBackDevice.EAR : AudioPlayBackDevice.SPEAKER;
};
const watchOptions = {
[NAME.PLAYER]: handlePlayerListChange,
[NAME.CALL_STATUS]: handleCallStatusChange,
[NAME.IS_EAR_PHONE]: handEarPhoneChange,
};
onMounted(() => {
TUIStore.watch(StoreName.CALL, watchOptions, { notifyRangeWhenWatch: NAME.MYSELF });
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CALL, watchOptions);
});
const switchScreen = (value,className) => {
if(callStatus.value !== CallStatus.CONNECTED || props.remoteClass === 'large-view') return;
TUICallKitServer.switchScreen(props.currentLocalScreen ? 'player':'localVideo');
};
function playerStateChange(e: any) {
TUICallKitServer._tuiCallEngine._playerStateChange(e);
}
function playerFullscreenChange(e: any) {
// TUICallKitServer._tuiCallEngine._playNetStatus(e);
}
function playNetStatus(e: any) {
TUICallKitServer._tuiCallEngine._playNetStatus(e);
}
function playerAudioVolumeNotify(e: any) {
TUICallKitServer._tuiCallEngine._playerAudioVolumeNotify(e);
}
</script>
<style lang="scss" src="./style/index.scss" scoped> </style>
@@ -0,0 +1,104 @@
<template>
<div :class="localClass">
<div @click="switchScreen" class="stream-box">
<live-pusher
class="stream"
:url="pusher.url"
:mode="pusher.mode"
:autopush="true"
:enable-camera="pusher.enableCamera"
:enable-mic="true"
:muted="!pusher.enableMic"
:enable-agc="true"
:enable-ans="true"
:enable-ear-monitor="pusher.enableEarMonitor"
auto-focus="false"
:zoom="pusher.enableZoom"
:min-bitrate="pusher.minBitrate"
:max-bitrate="pusher.maxBitrate"
:video-width="pusher.videoWidth"
:video-height="pusher.videoHeight"
:beauty="pusher.beautyLevel"
:whiteness="pusher.whitenessLevel"
:orientation="pusher.videoOrientation"
:aspect="pusher.videoAspect"
:device-position="pusher.frontCamera"
:remote-mirror="pusher.enableRemoteMirror"
:local-mirror="pusher.localMirror"
:background-mute="pusher.enableBackgroundMute"
:audio-quality="pusher.audioQuality"
:audio-volume-type="pusher.audioVolumeType"
:audio-reverb-type="pusher.audioReverbType"
:waiting-img="pusher.waitingImage"
:beauty-style="pusher.beautyStyle"
:filter="pusher.filter"
@statechange="pusherStateChangeHandler"
@netstatus="pusherNetStatus"
@error="pusherErrorHandler"
@audiovolumenotify="pusherAudioVolumeNotify"
></live-pusher>
</div>
</div>
</template>
<script lang="ts" setup>
import { TUICallKitServer, StoreName, NAME, TUIStore, CallStatus } from '../../../../../TUICallService/index';
import { onMounted, ref, onUnmounted } from '../../../../../adapter-vue';
const pusher = ref(TUIStore.getData(StoreName.CALL, NAME.PUSHER));
const callStatus = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS));
const props = defineProps({
localClass: {
type: String,
required: true,
},
currentLocalScreen: {
type: Boolean,
required: true,
},
});
const emit = defineEmits(['toggleViewSize']);
const switchScreen = (value,className) => {
if( callStatus.value !== CallStatus.CONNECTED || props.localClass === 'large-view') return;
TUICallKitServer.switchScreen(props.currentLocalScreen ? 'player':'localVideo');
};
const handlePusherChange = (value) => {
pusher.value = Object.assign({}, pusher.value, value);
};
const handleCallStatusChange = (value) => {
callStatus.value = value;
};
const watchOptions = {
[NAME.PUSHER]: handlePusherChange,
[NAME.CALL_STATUS]: handleCallStatusChange,
};
onMounted(() => {
TUIStore.watch(StoreName.CALL, watchOptions, { notifyRangeWhenWatch: NAME.MYSELF });
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CALL, watchOptions);
});
function pusherStateChangeHandler(e: any) {
TUICallKitServer._tuiCallEngine._pusherStateChangeHandler(e);
}
function pusherNetStatus(e: any) {
TUICallKitServer._tuiCallEngine._pusherNetStatus(e);
}
function pusherErrorHandler(e: any) {
TUICallKitServer._tuiCallEngine._pusherNetStatus(e);
}
function pusherAudioVolumeNotify(e: any) {
TUICallKitServer._tuiCallEngine._pusherAudioVolumeNotify(e);
}
</script>
<style lang="scss" src="./style/index.scss" scoped> </style>
@@ -0,0 +1,36 @@
.small-view {
position: fixed;
top: 8%;
right: 10%;
width: 40%;
height: 30%;
z-index: 999;
background-color: black;
.stream-box {
width: 100%;
height: 100%;
.stream {
width: 100%;
height: 100%;
}
}
}
.large-view {
width: 100%;
height: 100%;
background-color: black;
.stream-box {
width: 100%;
height: 100%;
.stream {
z-index: 1;
width: 100%;
height: 100%;
}
}
}
@@ -0,0 +1,214 @@
<template>
<div :class="bodyStyle">
<!-- 移动端主窗口 -->
<div v-if="isMobile" :class="['singleCall-mobile', videoStyle]">
<div v-if="(callStatus === CallStatus.CALLING || callType === CallMediaType.AUDIO) && !isShowFloatWindow" class="inviteHeader">
<InviteInfo></InviteInfo>
</div>
<PusherPlayer v-if="!isWeChat"></PusherPlayer>
<WeChatStream class="live-pusher" v-else></WeChatStream>
<BtnOperate v-if="!isShowFloatWindow" class="btnOperate btn-operate-mobile"></BtnOperate>
</div>
<!-- 桌面端主窗口 -->
<div v-if="!isMobile" v-show="!isShowFloatWindow" :class="['singleCall-desktop', videoStyle]">
<ToggleSize class="toggleSize" :allowed-full-screen="allowedFullScreen" :allowed-minimized="allowedMinimizedRef" @openFloatWindow="handOpenWindow"></ToggleSize>
<div v-if="callStatus === CallStatus.CALLING || callType === CallMediaType.AUDIO" class="inviteHeader">
<InviteInfo></InviteInfo>
</div>
<PusherPlayer></PusherPlayer>
<BtnOperate class="btnOperate"></BtnOperate>
</div>
<!-- 桌面端顶部菜单栏 -->
<div class="topToolbar-desk" v-if="!isMobile">
<Timer v-if="callStatus === CallStatus.CONNECTED && !isShowFloatWindow" class="topToolbar-time" :call-duration="callDuration"></Timer>
<FloatWindowDesk v-if="isShowFloatWindow" :local-user-info="localUserInfo" :callStatus="callStatus" :callRole="callRole" @closeFloatWindow="handCloseWindow"></FloatWindowDesk>
</div>
<!-- 移动端顶部菜单栏 -->
<div :class="isShowFloatWindow ? 'topToolbar-mobile-floatWindow' : 'topToolbar-mobile'" v-if="isMobile">
<ZoomOut v-if="allowedMinimizedRef && !isShowFloatWindow" :isGroupCall="isGroupCall" :callType="callType"></ZoomOut>
<FloatWindowMobile v-if="isShowFloatWindow && (callType === CallMediaType.AUDIO || isGroupCall)"></FloatWindowMobile>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, defineProps, onMounted, onUnmounted } from '../../../adapter-vue';
import InviteInfo from './InviteInfo/InviteInfo.vue';
import BtnOperate from './BtnOperate/BtnOperate.vue';
import PusherPlayer from './PusherPlayer/PusherPlayer.vue';
import WeChatStream from './PusherPlayer/WeChatStream.vue';
import FloatWindowDesk from '../common/FloatWindow/desktop/FloatWindow.vue';
import FloatWindowMobile from '../common/FloatWindow/mobile/FloatWindow.vue';
import ToggleSize from '../common/Button/ToggleSize.vue';
import Timer from '../common/Button/Timer.vue';
import ZoomOut from '../common/Button/ZoomOut.vue'
import { TUIGlobal, TUIStore, StoreName, TUICallKitServer, NAME, CallStatus, CallMediaType } from '../../../TUICallService/index';
const isMobile = !TUIGlobal.isPC;
const isWeChat = TUIGlobal.isWeChat;
const callStatus = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_STATUS));
const callType = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_MEDIA_TYPE));
const videoStyle = ref(!isWeChat && callType.value === CallMediaType.VIDEO ? 'videoStyle' : 'audioStyle');
const bodyStyle =ref(isWeChat ? 'weChatBody' : '')
const isShowFloatWindow = ref(false);
const localUserInfo = ref(TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO));
const callDuration = ref(TUIStore.getData(StoreName.CALL, NAME.CALL_DURATION));
const isGroupCall = ref(TUIStore.getData(StoreName.CALL, NAME.IS_GROUP));
const allowedMinimizedRef = ref(TUIStore.getData(StoreName.CALL, NAME.ENABLE_FLOAT_WINDOW));
const callRole = ref<string>(TUIStore.getData(StoreName.CALL, NAME.CALL_ROLE));
const props = defineProps({
allowedFullScreen: {
type: Boolean,
required: true,
},
});
const handleCallStatusChange = (value) => {
callStatus.value = value;
};
const handleCallMediaTypeChange = (value) => {
callType.value = value;
if (isWeChat) return;
videoStyle.value = callType.value === CallMediaType.VIDEO ? 'videoStyle' : '';
};
const handleLocalUserInfoChange = (value) => {
localUserInfo.value = value;
};
const handleIsShowFloatWindowChange = (value) => {
isShowFloatWindow.value = value;
};
const handleCallDurationChange = (value) => {
callDuration.value = value;
};
function handOpenWindow() {
TUICallKitServer.toggleMinimize();
}
function handCloseWindow() {
TUICallKitServer.toggleMinimize();
}
function handleEnableFloatWindowChange(value) {
allowedMinimizedRef.value = value;
}
const handleCallRoleChange = (value) => {
callRole.value = value;
};
onMounted(function () {
TUIStore.watch(StoreName.CALL, {
[NAME.CALL_STATUS]: handleCallStatusChange,
[NAME.CALL_MEDIA_TYPE]: handleCallMediaTypeChange,
[NAME.LOCAL_USER_INFO]: handleLocalUserInfoChange,
[NAME.IS_MINIMIZED]: handleIsShowFloatWindowChange,
[NAME.CALL_DURATION]: handleCallDurationChange,
[NAME.ENABLE_FLOAT_WINDOW]: handleEnableFloatWindowChange,
[NAME.CALL_ROLE]: handleCallRoleChange,
}, {
notifyRangeWhenWatch: NAME.MYSELF,
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CALL, {
[NAME.CALL_STATUS]: handleCallStatusChange,
[NAME.CALL_MEDIA_TYPE]: handleCallMediaTypeChange,
[NAME.LOCAL_USER_INFO]: handleLocalUserInfoChange,
[NAME.IS_MINIMIZED]: handleIsShowFloatWindowChange,
[NAME.CALL_DURATION]: handleCallDurationChange,
[NAME.ENABLE_FLOAT_WINDOW]: handleEnableFloatWindowChange,
[NAME.CALL_ROLE]: handleCallRoleChange,
});
});
</script>
<style lang="scss" scoped>
.btn-operate-mobile {
z-index: 99;
}
.videoStyle {
background: #00183c;
}
.audioStyle {
position: absolute;
background-color: white;
}
.singleCall-mobile {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
z-index: 0;
.inviteHeader {
width: 100%;
position: fixed;
top: 107px;
z-index: 99;
}
.live-pusher {
width: 100%;
height: 100%;
}
}
.singleCall-desktop {
width: 100%;
height: 100%;
text-align: center;
z-index: 0;
background: #00183c;
position: relative;
.toggleSize {
position: absolute;
right: 12px;
top: 12px;
z-index: 13;
cursor: pointer;
display: flex;
flex-direction: row;
justify-content: flex-start;
}
.inviteHeader {
position: absolute;
left: 50%;
top: 42%;
transform: translate(-50%, -50%);
z-index: 99;
}
}
.topToolbar-desk {
position: absolute;
top: 40px;
transform: translate(-50%, -50%);
left: 50%;
.topToolbar-time {
font-size: 16px;
font-weight: 500;
}
}
.topToolbar-mobile {
position: absolute;
top: 30px;
left: 20px;
z-index: 99;
}
.topToolbar-mobile-floatWindow{
position: absolute;
z-index: 99;
}
.weChatBody{
width: 100%;
height: 100%;
}
</style>
@@ -0,0 +1,32 @@
<template>
<IconBtn
:icon="acceptSrc"
:text="t('accept')"
:isMobile="isMobile"
@click="acceptEvent"
/>
</template>
<script lang="ts" setup>
import { TUICallKitServer, t, CallMediaType } from '../../../../TUICallService/index';
import acceptMobile from '../../../assets/mobile/accept.png';
import acceptAudioDesk from '../../../assets/desktop/acceptAudio.svg';
import acceptVideoDesk from '../../../assets/desktop/acceptVideo.svg';
import IconBtn from './Btn/IconBtn.vue';
const props = defineProps({
isMobile: {
type: Boolean,
required: true,
},
callType: {
type: Number,
required: true,
},
});
const acceptSrc = props.isMobile ? acceptMobile : props.callType === CallMediaType.AUDIO ? acceptAudioDesk : acceptVideoDesk;
const acceptEvent = async () => {
await TUICallKitServer.accept();
};
</script>
@@ -0,0 +1,123 @@
<template>
<div class="btn-container">
<div :class="btnBodyClassName" @click="handleClick" :style="{ width: btnBodyStyle.width, height: btnBodyStyle.height }">
<img v-if="loading" class="invite-calling-item-loading" :src="loadingIcon" />
<img v-if="icon && !loading" :src="icon" />
<slot />
</div>
<div :class="btnTextClassName">
{{ text }}
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from '../../../../../adapter-vue';
import loadingVideo from '../../../../assets/common/loading-video.png';
const props = defineProps({
text: {
type: String,
default: '',
},
icon: {
type: String,
},
size: {
type: Number,
},
loadingIcon: {
type: String,
default: loadingVideo,
},
loading: {
type: Boolean,
default: false,
},
isMobile: {
type: Boolean,
default: false,
}
});
const btnBodyClassName = computed(() => `btn-body ${props.isMobile ? 'mobile' : 'desktop'}`);
const btnTextClassName = computed(() => `btn-text ${props.isMobile ? 'mobile' : 'desktop'}`);
const emit = defineEmits(['click']);
const handleClick = () => {
!props.loading && emit('click');
};
const btnBodyStyle = computed(() => {
const width = props.size
? props.size
: props.isMobile ? 50 : 40;
const height = props.size
? props.size
: props.isMobile ? 50 : 40;
return { width: `${width}px`, height: `${height}px` };
});
</script>
<style lang="scss" scoped>
.btn-container {
display: flex;
flex-direction: column;
align-items: center;
width: 70px;
.btn-body {
cursor: pointer;
&.mobile {
display: flex;
align-items: center;
justify-content: center;
}
img {
width: 100%;
height: 100%;
}
image {
width: 100%;
height: 100%;
}
.invite-calling-item-loading {
width: 100%;
height: 100%;
animation: rotation 2s linear infinite;
}
@keyframes rotation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.invite-calling-item-loading img {
width: 100%;
height: 100%;
}
}
.btn-text {
margin-top: 5px;
white-space: nowrap;
font-weight: 400;
font-size: 16px;
line-height: 20px;
text-align: center;
&.desktop {
font-size: 14px;
}
}
}
</style>
@@ -0,0 +1,36 @@
/**
* @function 给目标元素添加事件监听
* @description 给目标元素添加事件监听
* @param {HTMLElement} 需要监听的元素,,为空默认window.document
* @param {String} 事件名
* @param {Function} 回调函数
* @param {Object} 监听事件的选项,同addEventListener的options参数
* @return {void} 返回停止监听的函数
* @example
* useListenEvent(btnEl, 'click', () => { console.log('click btn');});
*/
export function useListenEvent(event: string, handler: Function, options: object): () => any;
export function useListenEvent(target: HTMLElement, event: string, handler: Function, options: object): () => any;
export function useListenEvent(...args: any[]) {
let target = window?.document;
let event = '';
let handler = () => {};
let options = {};
if (typeof args[0] === 'string') {
[event, handler, options] = args;
} else {
[target, event, handler, options] = args;
}
if (!target) {
return;
}
target?.addEventListener(event, handler, options);
return () => {
target?.removeEventListener(event, handler, options);
};
}
@@ -0,0 +1,27 @@
import { onMounted, onUnmounted, unref } from '../../../../../../adapter-vue';
import { useListenEvent } from './useListenerEvent';
export function useOnClickOutSide(targetEls: any[], handler: () => any) {
let stop = () => {};
onMounted(() => {
const listener = (event: Event) => {
const targets = targetEls.map(targetEl => unref(targetEl));
const paths = event.composedPath();
const rs = [];
for (let i = 0; i < targets.length; i++) {
rs[i] = paths.includes(targets[i]);
}
if (rs.every(item => !item)) {
handler();
}
};
stop = useListenEvent('click', listener, { passive: true });
});
onUnmounted(() => {
stop?.();
});
}
@@ -0,0 +1,155 @@
<template>
<div class="camera-container">
<IconBtn
ref="btnRef"
:icon="cameraSrc"
:text="t('camera')"
:isMobile="isMobile"
@click="switchStatus"
>
</IconBtn>
<div ref="arrowRef" v-if="!isMobile" class="arrow-btn" @click="togglePopover">
<img class="arrow-img" :src="arrowSrc" />
</div>
<DeviceSelectPopover
v-if="!isMobile"
ref="deviceSelectRef"
:isMobile="isMobile"
:deviceType="DeviceType.CAMERA"
:showDeviceSelect="showDeviceSelect"
:targetBoundingClientRect="targetBoundingClientRect"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, watch } from '../../../../adapter-vue';
import { TUICallKitServer, TUIStore, StoreName, NAME, t } from '../../../../TUICallService/index';
import IconBtn from './Btn/IconBtn.vue';
import { DeviceType } from '../../../../TUICallService/const/call';
import cameraMobile from '../../../assets/mobile/camera-true.png';
import cameraCloseMobile from '../../../assets/mobile/camera-false.png';
import cameraDesk from '../../../assets/desktop/camera.svg';
import cameraCloseDesk from '../../../assets/desktop/camera-false.svg';
import arrowSrc from '../../../assets/desktop/arrow.png';
import DeviceSelectPopover from '../DeviceSelectPopover/DeviceSelectPopover.vue';
import { useOnClickOutSide } from './Btn/hooks/useOnClickOutSide';
const props = defineProps({
isMobile: {
type: Boolean,
required: true,
},
});
const btnRef = ref();
const cameraSrc = ref('');
const localUserInfo = ref(TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO));
const deviceSelectRef = ref();
const arrowRef = ref();
cameraSrc.value = props.isMobile ? cameraMobile : cameraDesk;
const targetBoundingClientRect = ref();
const showDeviceSelect = ref(false);
const togglePopover = async () => {
showDeviceSelect.value = !showDeviceSelect.value;
};
const replaceImg = async () => {
if (localUserInfo.value?.isVideoAvailable) {
props.isMobile ? (cameraSrc.value = cameraMobile) : (cameraSrc.value = cameraDesk);
} else {
props.isMobile ? (cameraSrc.value = cameraCloseMobile) : (cameraSrc.value = cameraCloseDesk);
}
};
watch(
() => localUserInfo.value?.isVideoAvailable,
() => {
replaceImg();
},
{ deep: true, immediate: true },
);
const switchStatus = async () => {
if (localUserInfo.value?.isVideoAvailable) {
await TUICallKitServer.closeCamera();
} else {
await TUICallKitServer.openCamera("localVideo");
}
};
useOnClickOutSide([deviceSelectRef, arrowRef], () => {
showDeviceSelect.value = false;
});
const handleLocalUserInfoChange = (value) => {
localUserInfo.value = value;
};
onMounted(() => {
targetBoundingClientRect.value = btnRef.value?.$el?.getBoundingClientRect?.() || {};
TUIStore.watch(StoreName.CALL, {
[NAME.LOCAL_USER_INFO]: handleLocalUserInfoChange,
}, {
notifyRangeWhenWatch: NAME.MYSELF,
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CALL, {
[NAME.LOCAL_USER_INFO]: handleLocalUserInfoChange,
});
});
</script>
<style lang="scss" scoped>
.scroll-container {
width: 100%;
overflow: hidden;
.scroll-content {
max-height: 60px;
margin-right: -20px;
overflow: hidden auto;
padding-right: 20px;
}
}
.camera-container {
position: relative;
display: flex;
.arrow-btn {
left: 43px;
width: 12px;
height: 12px;
margin-top: 7px;
padding: 5px 5px 5px 0;
cursor: pointer;
img {
width: 100%;
height: 100%;
}
}
}
.device-select-label {
width: 100%;
overflow: hidden;
padding: 5px 3px;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
text-align: left;
line-height: 20px;
cursor: pointer;
&.btn {
border-top: 1px solid #fff3;
}
&:hover {
border-radius: 5px;
background: #ffffff52;
}
}
</style>
@@ -0,0 +1,49 @@
<template>
<IconBtn
:loadingIcon="loading"
:loading="!isClickable"
:icon="hangupSrc"
:text="t('hangup')"
:isMobile="isMobile"
@click="hangupEvent"
/>
</template>
<script lang="ts" setup>
import { TUICallKitServer,t, CallMediaType,TUIGlobal } from '../../../../TUICallService/index';
import hangupMobile from '../../../assets/mobile/hangup.png';
import hangupDesk from '../../../assets/desktop/hangup.svg';
import loadingAudio from '../../../assets/common/loading-audio.svg';
import loadingVideo from '../../../assets/common/loading-video.png';
import IconBtn from './Btn/IconBtn.vue';
const props = defineProps({
isMobile: {
type: Boolean,
required: true,
},
isClickable: {
type: Boolean,
required: true,
},
isGroupCall: {
type: Boolean,
required: true,
},
callType: {
type: Number,
required: true,
},
});
const hangupSrc = props.isMobile ? hangupMobile : hangupDesk;
// 桌面端或者移动端 C2C 视频通话
const singleCondition = props.isMobile && !props.isGroupCall && props.callType === CallMediaType.VIDEO;
const loading = !props.isMobile || singleCondition ? loadingVideo : loadingAudio;
const hangupEvent = async () => {
await TUICallKitServer.hangup();
};
</script>
<style lang="scss" src="./style/index.scss" scoped>
</style>
@@ -0,0 +1,26 @@
<template>
<IconBtn
:icon="inviteUserSrc"
:text="t('invite member')"
:isMobile="isMobile"
@click="inviteUserEvent"
/>
</template>
<script lang="ts" setup>
import { TUIStore, StoreName, NAME, t } from '../../../../TUICallService/index';
import inviteUserPC from '../../../assets/desktop/invite_user.svg';
import inviteUserWX from '../../../assets/mobile/invite_user.svg';
import IconBtn from './Btn/IconBtn.vue';
const props = defineProps({
isMobile: {
type: Boolean,
required: true,
}
});
const inviteUserSrc = props.isMobile ? inviteUserWX : inviteUserPC;
const inviteUserEvent = async () => {
TUIStore.update(StoreName.CALL, NAME.SHOW_SELECT_USER, true);
};
</script>
@@ -0,0 +1,154 @@
<template>
<div class="camera-container">
<IconBtn
ref="btnRef"
:icon="micSrc"
:text="t('microphone')"
:isMobile="isMobile"
@click="switchStatus"
/>
<div ref="arrowRef" v-if="!isMobile" class="arrow-btn" @click="togglePopover">
<img class="arrow-img" :src="arrowSrc" />
</div>
<DeviceSelectPopover
v-if="!isMobile"
ref="deviceSelectRef"
:isMobile="isMobile"
:deviceType="DeviceType.MICROPHONE"
:showDeviceSelect="showDeviceSelect"
:targetBoundingClientRect="targetBoundingClientRect"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, watch, onMounted , onUnmounted } from '../../../../adapter-vue';
import { TUICallKitServer , TUIStore , StoreName , NAME , t } from '../../../../TUICallService/index';
import { DeviceType } from '../../../../TUICallService/const/call';
import micMobile from '../../../assets/mobile/audio-true.png';
import micCloseMobile from '../../../assets/mobile/audio-false.png';
import micDesk from '../../../assets/desktop/microphone.svg';
import micCloseDesk from '../../../assets/desktop/microphone-false.svg';
import IconBtn from './Btn/IconBtn.vue';
import arrowSrc from '../../../assets/desktop/arrow.png';
import DeviceSelectPopover from '../DeviceSelectPopover/DeviceSelectPopover.vue';
import { useOnClickOutSide } from './Btn/hooks/useOnClickOutSide';
const props = defineProps({
isMobile: {
type: Boolean,
required: true,
},
});
const btnRef = ref();
const localUserInfo = ref(TUIStore.getData(StoreName.CALL, NAME.LOCAL_USER_INFO));
const deviceSelectRef = ref();
const arrowRef = ref();
const micSrc = ref('');
micSrc.value = props.isMobile ? micMobile : micDesk;
const replaceImg = async () => {
if (localUserInfo.value?.isAudioAvailable) {
props.isMobile ? (micSrc.value = micMobile) : (micSrc.value = micDesk);
} else {
props.isMobile ? (micSrc.value = micCloseMobile) : (micSrc.value = micCloseDesk);
}
};
const targetBoundingClientRect = ref();
const showDeviceSelect = ref(false);
const togglePopover = async () => {
showDeviceSelect.value = !showDeviceSelect.value;
};
watch(
() => localUserInfo.value?.isAudioAvailable,
() => {
replaceImg();
},
{ deep: true, immediate: true },
);
const switchStatus = async () => {
if (localUserInfo.value?.isAudioAvailable) {
await TUICallKitServer.closeMicrophone();
} else {
await TUICallKitServer.openMicrophone();
}
};
useOnClickOutSide([deviceSelectRef, arrowRef], () => {
showDeviceSelect.value = false;
});
const handleLocalUserInfoChange = (value) => {
localUserInfo.value = value;
};
onMounted(() => {
targetBoundingClientRect.value = btnRef.value?.$el?.getBoundingClientRect?.() || {};
TUIStore.watch(StoreName.CALL, {
[NAME.LOCAL_USER_INFO]: handleLocalUserInfoChange,
}, {
notifyRangeWhenWatch: NAME.MYSELF,
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CALL, {
[NAME.LOCAL_USER_INFO]: handleLocalUserInfoChange,
});
});
</script>
<style lang="scss" scoped>
.scroll-container {
width: 100%;
overflow: hidden;
.scroll-content {
max-height: 60px;
margin-right: -20px;
overflow: hidden auto;
padding-right: 20px;
}
}
.camera-container {
position: relative;
display: flex;
.arrow-btn {
left: 43px;
width: 12px;
height: 12px;
margin-top: 7px;
padding: 5px 5px 5px 0;
cursor: pointer;
img {
width: 100%;
height: 100%;
}
}
}
.device-select-label {
width: 100%;
overflow: hidden;
padding: 5px 3px;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
text-align: left;
line-height: 20px;
cursor: pointer;
&.btn {
border-top: 1px solid #fff3;
}
&:hover {
border-radius: 5px;
background: #ffffff52;
}
}
</style>
@@ -0,0 +1,27 @@
<template>
<IconBtn
:icon="hangupSrc"
:text="t('reject')"
:isMobile="isMobile"
@click="rejectEvent"
/>
</template>
<script lang="ts" setup>
import { TUICallKitServer , t } from '../../../../TUICallService/index';
import rejectMobile from '../../../assets/mobile/hangup.png';
import rejectDesk from '../../../assets/desktop/hangup.svg';
import IconBtn from './Btn/IconBtn.vue';
const props = defineProps({
isMobile: {
type: Boolean,
required: true,
},
});
const hangupSrc = props.isMobile ? rejectMobile : rejectDesk;
const rejectEvent = async () => {
await TUICallKitServer.reject();
};
</script>
@@ -0,0 +1,37 @@
<template>
<IconBtn
:icon="speakerSrc"
:text="t('speaker')"
:isMobile="true"
@click="switchStatus"
/>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from '../../../../adapter-vue';
import { TUICallKitServer, TUIStore, StoreName, NAME, t } from '../../../../TUICallService/index';
import speakerMobile from '../../../assets/mobile/speaker-true.png';
import speakerCloseMobile from '../../../assets/mobile/speaker-false.png';
import IconBtn from './Btn/IconBtn.vue';
const speakerSrc = ref(TUIStore.getData(StoreName.CALL, NAME.IS_EAR_PHONE) ? speakerCloseMobile : speakerMobile);
const handEarPhoneChange = (value) => {
speakerSrc.value = value ? speakerCloseMobile : speakerMobile;
};
onMounted(() => {
TUIStore.watch(StoreName.CALL, {
[NAME.IS_EAR_PHONE]: handEarPhoneChange,
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CALL, {
[NAME.IS_EAR_PHONE]: handEarPhoneChange,
});
});
const switchStatus = () => {
TUICallKitServer.setSoundMode();
};
</script>
@@ -0,0 +1,93 @@
<template>
<IconBtn
:class="bodyStyle"
:style="{ width: '40px' }"
:size="iconSize"
:text="t('video-to-audio')"
:isMobile="isMobile"
@click="switchAudioCall"
>
<div class="call-operate">
<img class="operate-img" :src="transSrc" />
</div>
</IconBtn>
</template>
<script lang="ts" setup>
import { TUICallKitServer } from '../../../../TUICallService/index';
import transMobile from '../../../assets/mobile/trans.png';
import transDesk from '../../../assets/desktop/trans.svg';
import { t } from '../../../../TUICallService/index';
import IconBtn from './Btn/IconBtn.vue';
const props = defineProps({
isMobile: {
type: Boolean,
required: true,
},
});
const transSrc = props.isMobile ? transMobile : transDesk;
const bodyStyle = props.isMobile ? 'btn-operate-mobile' : 'btn-operate-desktop';
const iconSize = props.isMobile ? 28 : 40;
const switchAudioCall = async () => {
await TUICallKitServer.switchCallMediaType();
};
</script>
<style lang="scss" scoped>
.btn-operate-desktop {
.call-operate {
display: flex;
flex-direction: column;
width: 40px;
height: 40px;
cursor: pointer;
}
.operate-img {
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
position: relative;
}
span {
display: block;
margin-top: 5px;
white-space: nowrap;
font-weight: 400;
font-size: 14px;
line-height: 20px;
text-align: center;
}
}
.btn-operate-mobile {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
.call-operate {
width: 28px;
height: 26px;
border-radius: 8vh;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
.operate-img {
width: 100%;
height: 100%;
background: none;
}
}
span {
margin-top: 10px;
}
}
</style>
@@ -0,0 +1,34 @@
<template>
<IconBtn
:icon="switchCamera"
:size="32"
text=" "
:isMobile="true"
@click="frontCamera"
/>
</template>
<script lang="ts" setup>
import switchCamera from '../../../assets/mobile/switch_camera.png';
import { TUICallKitServer } from '../../../../TUICallService/index';
import IconBtn from './Btn/IconBtn.vue';
function frontCamera() {
TUICallKitServer.switchCamera();
}
</script>
<style lang="scss" scoped>
.btn-operate-item {
position: absolute;
width: 100%;
height: 100%;
top: 10px;
right: -100px;
.operate-img {
width: 32px;
height: 32px;
}
}
</style>
@@ -0,0 +1,19 @@
<template>
<span class="text">{{ callDuration }}</span>
</template>
<script lang="ts" setup>
const props = defineProps({
callDuration: {
type: String,
required: true,
},
});
</script>
<style lang="scss" scoped>
.text {
display: block;
margin-bottom: 20px;
}
</style>
@@ -0,0 +1,50 @@
<template>
<div class="function-buttons">
<div v-if="allowedMinimized" class="minimize" @click="toggleMinimize">
<img :src="MinimizeSVG" />
</div>
<div v-if="allowedFullScreen" class="minimize" @click="toggleFullscreen">
<img :src="fullScreenSVG" />
</div>
</div>
</template>
<script lang="ts" setup>
import MinimizeSVG from '../../../assets/desktop/minimize.svg';
import fullScreenSVG from '../../../assets/desktop/fullScreen.svg';
const emit = defineEmits(['openFloatWindow']);
function toggleMinimize() {
if (document.fullscreenElement) {
document.exitFullscreen();
}
emit('openFloatWindow');
}
const props = defineProps({
allowedFullScreen: {
type: Boolean,
required: true,
},
allowedMinimized: {
type: Boolean,
required: true,
},
});
function toggleFullscreen() {
const elem = document.getElementById('tui-call-kit-id') as HTMLElement;
if (!document.fullscreenElement) {
elem.requestFullscreen().catch((err) => {
alert(`Error attempting to enable fullscreen mode: ${err.message} (${err.name})`);
});
} else {
document.exitFullscreen();
}
}
</script>
<style scoped>
.minimize {
margin-left: 10px;
}
</style>
@@ -0,0 +1,59 @@
<template>
<div class="zoom-box" @click="toggleMinimize">
<img class="zoom-img" :src="zoomSrc" />
</div>
</template>
<script lang="ts" setup>
import { onUnmounted, onMounted, ref } from '../../../../adapter-vue';
import zoomVideo from '../../../assets/mobile/zoom_out_video.svg';
import zoomAudio from '../../../assets/mobile/zoom_out_audio.svg';
import { TUICallKitServer, CallMediaType, TUIStore, StoreName, NAME,} from '../../../../TUICallService/index';
const props = defineProps({
isGroupCall: {
type: Boolean,
required: true,
},
callType: {
type: Number,
required: true,
},
});
let zoomSrc = ref(props.isGroupCall || props.callType === CallMediaType.AUDIO ? zoomAudio : zoomVideo);
function toggleMinimize() {
TUICallKitServer.toggleMinimize();
}
onMounted(() => {
TUIStore.watch(StoreName.CALL, {
[NAME.CALL_MEDIA_TYPE]: handleCallMediaTypeChange,
}, {
notifyRangeWhenWatch: NAME.MYSELF,
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CALL, {
[NAME.CALL_MEDIA_TYPE]: handleCallMediaTypeChange,
});
});
const handleCallMediaTypeChange = (value) =>{
zoomSrc.value = (value === CallMediaType.AUDIO ? zoomAudio : zoomVideo);
}
</script>
<style lang="scss" scoped>
.zoom-box {
display: block;
width: 20px;
height: 20px;
.zoom-img {
width: 100%;
height: 100%;
}
}
</style>

Some files were not shown because too many files have changed in this diff Show More