Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c5c442928 | ||
|
|
2380e56266 | ||
|
|
fdce66d761 | ||
|
|
34e1590382 | ||
|
|
7fea1a0d9f | ||
|
|
8cf3ee079b | ||
|
|
388c6bdf4a | ||
|
|
6ca7141c5d | ||
|
|
687fb37beb | ||
|
|
f269c42a58 | ||
|
|
1cd7a43bc5 | ||
|
|
43e37e3c54 | ||
|
|
e897357108 | ||
|
|
a416e9d7df | ||
|
|
ae3a3bcab6 | ||
|
|
6ae5e62d42 | ||
|
|
6e269ac987 | ||
|
|
31bdef36ea | ||
|
|
82ade3d3d4 | ||
|
|
b9c7b6c1ac | ||
|
|
99ba667bbb | ||
|
|
9711d55edd | ||
|
|
2bd4cd399e | ||
|
|
d573fe5fba | ||
|
|
b26398abef | ||
|
|
f0637d0fa1 | ||
|
|
0c3e495e53 | ||
|
|
be61c536b3 | ||
|
|
bb41ae6afa | ||
|
|
62dc4e5e82 | ||
|
|
10e834f68e | ||
|
|
0357c6aa8b | ||
|
|
8a4113f5d4 | ||
|
|
f696bac580 | ||
|
|
ad1da10c9c | ||
|
|
156d810329 | ||
|
|
f96c3655df | ||
|
|
9f6e34d7de | ||
|
|
3d4177b647 | ||
|
|
98a2680b63 | ||
|
|
ef852d13d6 | ||
|
|
119f75f550 | ||
|
|
c4f9b4680f | ||
|
|
c94c567c60 | ||
|
|
557d013095 | ||
|
|
72074f59e0 | ||
|
|
f4574a5df7 | ||
|
|
0b9c9d497e | ||
|
|
66a0c2f4d6 | ||
|
|
6125c6fc87 | ||
|
|
e629dc3fb0 | ||
|
|
80f7434a37 | ||
|
|
2e23b3641c | ||
|
|
39d1ccf4bd | ||
|
|
3e92603025 | ||
|
|
ae8e8e7e8f | ||
|
|
081eb910eb | ||
|
|
5afe560654 | ||
|
|
8177bed6e2 | ||
|
|
7510588122 | ||
|
|
c512766e95 | ||
|
|
6ed0a91f5d | ||
|
|
d85e40e83d | ||
|
|
063b0a5efd | ||
|
|
4fd7832eef | ||
|
|
11488335d1 | ||
|
|
5f9c7a073b | ||
|
|
3db7b27796 | ||
|
|
751e07d747 | ||
|
|
985847eb7f | ||
|
|
f5bd3b9646 | ||
|
|
fb2a6b1c6f | ||
|
|
512d6cd5b9 | ||
|
|
8beac20793 | ||
|
|
3c49fc9d1e | ||
|
|
91f2c612e8 | ||
|
|
8a9ea4753e | ||
|
|
78141a788f | ||
|
|
308d9ae94e | ||
|
|
08b9144f8c | ||
|
|
be946d3f0b | ||
|
|
f0a17ff9f1 | ||
|
|
d1736dfe25 | ||
|
|
9cb6a0ad7a | ||
|
|
534e759350 |
@@ -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=
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;"
|
||||
@@ -32,7 +32,7 @@
|
||||
<script>
|
||||
window._CONFIG = {};
|
||||
window._AMapSecurityConfig = {
|
||||
securityJsCode: "1b3b86585c863d22db1e7f7636a9e495",
|
||||
securityJsCode: "54f1d830d2bb225c173b76f5780b65eb",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
src/TUIKit/TUIPlugin/TUICallKit/*
|
||||
src/TUIKit/**
|
||||
|
||||
@@ -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,所以服务器计算的方案能够更好地保护您的加密密钥。
|
||||
*
|
||||
* Reference:https://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;
|
||||
@@ -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 |
|
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 |
|
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 };
|
||||
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
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 |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1017 B |
|
After Width: | Height: | Size: 974 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
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 |
|
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 |
|
After Width: | Height: | Size: 821 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 799 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
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>
|
||||