feat: 开发消息功能 (#11)
* feat: back title * feat: 本地联调 * feat: 聊天室初始化,以及联动query * feat: useSocket,online users count * feat: 调整useSocket、useCreateRoom(优化hook) * feat: 拆分roomInfo、roomConnect 的 hook * feat: chat room 基本布局 * feat: chat * fix: 样式 + emoji * feat: 拖拽改变元素大小的hook,一些样式fix * feat: video chat init * feat: 调试摄像头、audio信息 * feat: video的一些设置,代码优化 * fix: 样式 * feat: 视频聊天的连接测试 * feat: https * feat: 多人视频通话,待优化整理代码 * feat: 代码优化、边界处理 * feat: 优化一轮使用体验 * feat: 构建相关
2
.gitignore
vendored
@@ -53,3 +53,5 @@ package-lock.json
|
||||
|
||||
|
||||
docker/mysql/data/*
|
||||
|
||||
client_dist
|
2
client/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
@@ -4,7 +4,8 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev:web": "pnpm -C packages/rtc-web dev"
|
||||
"dev:web": "pnpm -C packages/rtc-web dev",
|
||||
"build:web": "pnpm -C packages/rtc-web build"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
@@ -18,10 +18,11 @@ module.exports = {
|
||||
},
|
||||
plugins: ['vue', '@typescript-eslint', 'prettier'],
|
||||
rules: {
|
||||
indent: ['error', 2],
|
||||
indent: ['error', 2, { offsetTernaryExpressions: true }],
|
||||
semi: ['error', 'always'],
|
||||
'vue/attribute-hyphenation': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'no-debugger': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
},
|
||||
};
|
||||
|
@@ -11,10 +11,15 @@
|
||||
"@types/lodash": "^4.14.195",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
||||
"@vueuse/core": "^10.2.0",
|
||||
"@vueuse/router": "^10.2.1",
|
||||
"dayjs": "^1.11.9",
|
||||
"lodash": "^4.17.21",
|
||||
"nanoid": "^4.0.2",
|
||||
"socket.io-client": "2.3.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "4"
|
||||
"vue-router": "4",
|
||||
"vue3-emoji-picker": "^1.1.7",
|
||||
"vue3-popper": "^1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.3.1",
|
||||
@@ -32,7 +37,9 @@
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"terser": "^5.19.4",
|
||||
"typescript": "^5.1.3",
|
||||
"vconsole": "^3.15.1",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
|
@@ -1,8 +1,20 @@
|
||||
<script lang="ts" setup>
|
||||
import Layout from '@/layout/index.vue';
|
||||
import ErrorBoundary from '@/components/error-boundary/index.vue';
|
||||
import { useErrorCaptured } from '@/hooks';
|
||||
|
||||
const errors = useErrorCaptured();
|
||||
</script>
|
||||
|
||||
<!-- 这里后面可以加 provider、区分登录和未登录页面 -->
|
||||
<template>
|
||||
<Layout />
|
||||
<Suspense>
|
||||
<Layout />
|
||||
<template v-if="errors.length" #fallback>
|
||||
<ErrorBoundary
|
||||
class="h-screen"
|
||||
:tips-list="['服务器开小差了,请联系管理员']"
|
||||
/>
|
||||
</template>
|
||||
</Suspense>
|
||||
</template>
|
||||
|
@@ -1 +1,21 @@
|
||||
@import './tailwindcss';
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
1
client/packages/rtc-web/src/assets/svg-icon/audio.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1691553735044" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1412" width="200" height="200"><path d="M483.363 517.9h-53.357v53.348h53.357V517.9z m-160.051 0v53.348h53.348V517.9h-53.348z m213.398 53.348h53.348V517.9H536.71v53.348z m160.051 0V517.9h-53.347v53.348h53.347z m-266.755 53.365v53.348h53.357v-53.348h-53.357z m-106.694 0l19.802 53.348h33.545v-53.348h-53.347z m213.398 53.348h53.348v-53.348H536.71v53.348z m-106.704 69.748l53.357 10.273v-26.674h-53.357v16.401z m106.704 5.514l53.348-8.576v-13.338H536.71v21.914z m106.704-75.262h38.922l14.425-53.348h-53.347v53.348zM536.71 464.553h-53.347V517.9h53.347v-53.347z m-106.704 0h-53.347V517.9h53.347v-53.347zM590.058 517.9h53.356v-53.347h-53.356V517.9zM483.363 624.613h53.347v-53.365h-53.347v53.365z m-53.357-53.365h-53.347v53.365h53.347v-53.365z m160.052 53.365h53.356v-53.365h-53.356v53.365z m-106.695 53.348v53.348h53.347v-53.348h-53.347z m-106.704 0v36.746l53.347 16.602v-53.348h-53.347z m213.399 53.348l53.356-17.449v-35.898h-53.356v53.347z m106.703-160.061v53.365l17.414-35.896v-17.469h-17.414zM536.71 464.553h53.348v-53.348H536.71v53.348z m160.051 0v-53.348h-53.347v53.348h53.347z m-213.398-53.347h-53.357v53.348h53.357v-53.348z m-160.051 0v53.348h53.348v-53.348h-53.348z m106.694-53.348h-53.347v53.348h53.347v-53.348z m106.704 0h-53.347v53.348h53.347v-53.348z m53.348 53.348h53.356v-53.348h-53.356v53.348z m-53.348-53.348h53.348v-53.347H536.71v53.347z m160.051 0v-53.347h-53.347v53.347h53.347z m-213.398-53.347h-53.357v53.347h53.357v-53.347z m-160.051 0v53.347h53.348v-53.347h-53.348z m106.694-53.347h-53.347v53.347h53.347v-53.347z m106.704 0h-53.347v53.347h53.347v-53.347z m53.348 53.347h53.356v-53.347h-53.356v53.347z m-53.348-53.347h53.348v-53.366H536.71v53.366z m160.051 0v-53.366h-53.347v53.366h53.347z m-213.398-53.367h-53.357v53.366h53.357v-53.366z m-160.051 0v53.366h53.348v-53.366h-53.348z m106.694-53.347h-53.347v53.347h53.347V144.45z m106.704 53.347V144.45h-53.347v53.347h53.347z m53.348 0h53.356V144.45h-53.356v53.347z m0-53.347V91.103H536.71v53.347h53.348z m106.703 0l-53.347-53.347v53.347h53.347z m-213.398 0V91.103h-53.357v53.347h53.357zM363.58 104.846l-40.269 39.604h53.348V91.103l-13.079 13.743z m173.13-67.091h-53.347v53.348h53.347V37.755zM430.006 64.429l-26.674 13.337-26.673 13.337h53.347V64.429zM624.092 78.08l-34.034-13.65v26.674h53.356L624.092 78.08z" fill="" p-id="1413"></path><path d="M847.257 554.738c0-16.934-5.533-27.965-15.551-27.965-10.938 0-15.568 11.031-15.568 27.965 0 162.92-169.403 303.777-306.166 303.777-155.836 0-305.77-152-305.77-304.184 0-16.934-9.233-21.527-15.098-21.527-6.42 0-16.427 5-16.427 21.934 0 175.906 151.252 320.232 323.128 335.746v72.512h-169.81c-16.952 0-30.667 5.609-30.667 18.281 0 11.824 13.715 15.365 30.667 15.365h367.953c16.953 0 30.667-2.951 30.667-15.346 0-13.283-13.714-18.301-30.667-18.301H526.499v-72.512c171.877-15.512 320.758-159.838 320.758-335.745zM509.972 769.383c118.547 0 214.644-96.125 214.644-214.645V248.12c0-118.537-96.097-214.644-214.644-214.644-118.546 0-214.644 96.106-214.644 214.644v306.619c0 118.519 96.097 214.644 214.644 214.644zM328.163 248.12c0-101.197 75.852-183.248 181.809-183.248 97.397 3.247 180.73 75.262 183.387 183.248v306.619c0 97.971-73.104 185.332-183.387 185.332-110.394 0-181.809-85.408-181.809-185.332V248.12z" p-id="1414"></path></svg>
|
After Width: | Height: | Size: 3.3 KiB |
1
client/packages/rtc-web/src/assets/svg-icon/back.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1689645854295" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1042" width="200" height="200"><path d="M947.4 864C893.2 697.7 736.2 578.9 551 575.5c-23.1-0.4-44.9 0.1-65.6 1.5v164.3c0.1 0.5 0.2 1 0.2 1.5 0 4-3.3 7.3-7.3 7.3-2.7 0-5-1.4-6.2-3.5v0.7L68.8 465.4h2.1c-4 0-7.3-3.3-7.3-7.3 0-2.9 1.7-5.4 4.1-6.6L472 169v0.7c1.3-2.1 3.6-3.5 6.2-3.5 4 0 7.3 3.3 7.3 7.3 0 0.5-0.1 1-0.2 1.5v159.4c18.5-0.9 37.9-1.2 58.3-0.8 230.1 3.9 416.7 196.9 416.7 427.1 0.1 35.5-4.5 70.2-12.9 103.3z m-462-704.4v0.2h-0.4l0.4-0.2z m0 596.9l-0.3-0.2h0.3v0.2z" fill="#2F54EB" p-id="1043"></path></svg>
|
After Width: | Height: | Size: 629 B |
1
client/packages/rtc-web/src/assets/svg-icon/camera.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1691553222480" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1258" width="200" height="200"><path d="M704 768V256H128v512h576z m64-416L960 256v512l-192-96v128a31.168 31.168 0 0 1-8.96 23.04 31.168 31.168 0 0 1-23.04 8.96h-640a31.168 31.168 0 0 1-23.04-8.96 31.168 31.168 0 0 1-8.96-23.04v-576c0-9.344 3.008-17.024 8.96-23.04A31.168 31.168 0 0 1 96 192h640c9.344 0 17.024 3.008 23.04 8.96 5.952 6.016 8.96 13.696 8.96 23.04v128z m0 72v176l128 64v-304l-128 64zM192 320h192v64H192V320z" p-id="1259"></path></svg>
|
After Width: | Height: | Size: 563 B |
1
client/packages/rtc-web/src/assets/svg-icon/chat.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1689646256077" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2749" width="200" height="200"><path d="M512 64c259.2 0 469.333333 200.576 469.333333 448s-210.133333 448-469.333333 448a484.48 484.48 0 0 1-232.725333-58.88l-116.394667 50.645333a42.666667 42.666667 0 0 1-58.517333-49.002666l29.76-125.013334C76.629333 703.402667 42.666667 611.477333 42.666667 512 42.666667 264.576 252.8 64 512 64z m0 64C287.488 128 106.666667 300.586667 106.666667 512c0 79.573333 25.557333 155.434667 72.554666 219.285333l5.525334 7.317334 18.709333 24.192-26.965333 113.237333 105.984-46.08 27.477333 15.018667C370.858667 878.229333 439.978667 896 512 896c224.512 0 405.333333-172.586667 405.333333-384S736.512 128 512 128z m-157.696 341.333333a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334z m159.018667 0a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334z m158.997333 0a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334z" fill="#2F54EB" p-id="2750"></path></svg>
|
After Width: | Height: | Size: 1.1 KiB |
1
client/packages/rtc-web/src/assets/svg-icon/count.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1689914473474" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1272" width="200" height="200"><path d="M514.048 128q79.872 0 149.504 30.208t121.856 82.432 82.432 122.368 30.208 150.016q0 78.848-30.208 148.48t-82.432 121.856-121.856 82.432-149.504 30.208-149.504-30.208-121.856-82.432-82.432-121.856-30.208-148.48q0-79.872 30.208-150.016t82.432-122.368 121.856-82.432 149.504-30.208z" p-id="1273" fill="#0CC6BF"></path></svg>
|
After Width: | Height: | Size: 476 B |
1
client/packages/rtc-web/src/assets/svg-icon/emoji.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1689752584815" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6347" width="200" height="200"><path d="M480 452.266667m-426.666667 0a426.666667 426.666667 0 1 0 853.333334 0 426.666667 426.666667 0 1 0-853.333334 0Z" fill="#FFE500" p-id="6348"></path><path d="M480 25.6c-234.666667 0-426.666667 192-426.666667 426.666667s192 426.666667 426.666667 426.666666 426.666667-192 426.666667-426.666666-192-426.666667-426.666667-426.666667z m0 789.333333C264.533333 814.933333 91.733333 640 91.733333 426.666667c0-215.466667 174.933333-388.266667 388.266667-388.266667S870.4 211.2 870.4 426.666667c0 213.333333-174.933333 388.266667-390.4 388.266666z" fill="#EBCB00" p-id="6349"></path><path d="M352 110.933333a128 32 0 1 0 256 0 128 32 0 1 0-256 0Z" fill="#FFF48C" p-id="6350"></path><path d="M138.666667 966.4a341.333333 32 0 1 0 682.666666 0 341.333333 32 0 1 0-682.666666 0Z" fill="#45413C" opacity=".15" p-id="6351"></path><path d="M170.666667 558.933333a53.333333 32 0 1 0 106.666666 0 53.333333 32 0 1 0-106.666666 0Z" fill="#FFAA54" p-id="6352"></path><path d="M277.333333 409.6m-21.333333 0a21.333333 21.333333 0 1 0 42.666667 0 21.333333 21.333333 0 1 0-42.666667 0Z" fill="#45413C" p-id="6353"></path><path d="M693.333333 409.6m-21.333333 0a21.333333 21.333333 0 1 0 42.666667 0 21.333333 21.333333 0 1 0-42.666667 0Z" fill="#45413C" p-id="6354"></path><path d="M384 729.6s51.2-21.333333 96-21.333333 96 21.333333 96 21.333333M512 526.933333h416c23.466667 0 42.666667 19.2 42.666667 42.666667s-19.2 42.666667-42.666667 42.666667H512" fill="#FFE500" p-id="6355"></path></svg>
|
After Width: | Height: | Size: 1.6 KiB |
1
client/packages/rtc-web/src/assets/svg-icon/hang-up.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1691555214679" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2779" width="200" height="200"><path d="M512 1023.999069a508.779681 508.779681 0 0 1-199.291759-40.238472 510.300163 510.300163 0 0 1-162.745886-109.73071A510.354466 510.354466 0 0 1 40.231646 711.284001 508.826226 508.826226 0 0 1 0.000931 512a508.841741 508.841741 0 0 1 40.238472-199.291759 510.338951 510.338951 0 0 1 109.73071-162.745886A510.292406 510.292406 0 0 1 312.708241 40.239403 508.787439 508.787439 0 0 1 512 0.000931a508.795196 508.795196 0 0 1 199.291759 40.238472 510.222587 510.222587 0 0 1 162.745886 109.722952 510.284648 510.284648 0 0 1 109.730709 162.745886A508.841741 508.841741 0 0 1 1023.999069 512a508.826226 508.826226 0 0 1-40.238472 199.291759 510.292406 510.292406 0 0 1-109.73071 162.745886 510.27689 510.27689 0 0 1-162.745886 109.730709A508.795196 508.795196 0 0 1 512 1023.999069zM252.695744 543.348307a13.4904 13.4904 0 0 0 3.878781 9.471982 13.335248 13.335248 0 0 0 9.471983 3.917569l127.456738-0.201697a13.428339 13.428339 0 0 0 13.412824-13.412824l0.06206-51.680875a87.272569 87.272569 0 0 1 24.420804-20.030025 161.489161 161.489161 0 0 1 82.493911-20.16966h0.279272a159.573043 159.573043 0 0 1 82.230154 20.084327 86.985539 86.985539 0 0 1 24.296683 20.084327v51.362816a13.358521 13.358521 0 0 0 3.878781 9.471983 13.226643 13.226643 0 0 0 9.425437 3.917568l114.400762-0.170666h13.110279a13.358521 13.358521 0 0 0 9.495255-3.917569 13.451612 13.451612 0 0 0 3.878781-9.495255l0.054303-29.168432a13.226643 13.226643 0 0 0-0.387878-3.157327l-0.558544-2.327269a88.436203 88.436203 0 0 0-6.826655-27.570374c-25.553408-59.453953-131.288973-104.292659-245.914704-104.292659h-0.589575a511.355191 511.355191 0 0 0-134.903997 18.967238c-45.102463 12.776704-80.802762 30.681156-100.514726 50.424151a114.990336 114.990336 0 0 0-24.901773 35.645996 86.357176 86.357176 0 0 0-7.57138 34.505634l-0.054303 27.787586z" fill="#FC4A46" p-id="2780"></path></svg>
|
After Width: | Height: | Size: 2.0 KiB |
1
client/packages/rtc-web/src/assets/svg-icon/member.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1689750291003" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2521" width="200" height="200"><path d="M837.603556 277.134222a2.275556 2.275556 0 0 0-0.056889-0.483555c-0.199111-0.654222 14.848 44.856889-0.597334-1.934223a88.462222 88.462222 0 0 0-0.995555-2.929777c-14.961778-42.496-39.224889-58.965333-57.002667-62.321778-22.840889-4.352-47.587556 5.774222-47.587555 35.498667 0 4.437333 0.853333 9.272889 2.417777 14.250666l-0.142222-0.028444c0.085333 0.256 0.227556 0.512 0.369778 0.768 1.621333 5.063111 3.953778 10.268444 6.769778 15.445333 13.767111 34.019556 21.333333 71.224889 21.333333 110.165333 0 76.117333-28.956444 145.521778-76.458667 197.802667 102.030222 60.273778 177.208889 184.149333 203.064889 303.672889H981.333333c-21.162667-119.978667-82.915556-248.632889-170.524444-325.632a385.194667 385.194667 0 0 0 42.154667-175.843556 386.275556 386.275556 0 0 0-15.36-108.430222zM429.056 637.013333a250.026667 250.026667 0 1 0-0.028444-500.081777 250.026667 250.026667 0 0 0 0.028444 500.081777z m158.151111-253.354666a161.479111 161.479111 0 1 1-322.958222 0 161.479111 161.479111 0 0 1 322.958222 0z m182.784 503.381333c-30.663111-180.878222-180.707556-340.935111-352.483555-340.935111-191.744 0-343.950222 160.056889-374.840889 340.935111h91.306666c29.354667-130.304 141.624889-250.026667 283.534223-250.026667 122.737778 0 232.248889 119.694222 261.205333 250.026667h91.278222z" p-id="2522"></path></svg>
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1 @@
|
||||
<svg t="1691555195526" class="icon" viewBox="0 0 1152 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2625" width="200" height="200"><path d="M1091.242667 116.032c31.722667 10.005333 54.592 39.253333 54.741333 73.536v630.250667c0 28.928-16.618667 55.424-43.029333 68.672L714.368 1006.72c-4.309333 0.704-8.597333 1.28-13.162667 1.28-35.029333 0-63.488-27.733333-63.488-61.994667V61.845333C637.717333 27.733333 666.176 0 701.205333 0c4.416 0 8.832 0.405333 13.162667 1.258667L1093.504 116.693333c-0.554667-0.128-1.429333-0.426667-2.261333-0.682666z m-40.618667 676.757333V215.04L733.077333 132.586667v742.656l317.546667-82.453334zM450.944 0c35.2 0 63.637333 27.733333 63.338667 61.845333v884.16c0 34.282667-28.437333 61.994667-63.466667 61.994667-4.48 0-8.917333-0.426667-13.312-1.258667L49.194667 888.490667C23.594667 875.52 6.186667 849.621333 6.186667 819.690667V189.717333c0-35.52 24.746667-65.493333 58.346666-74.517333-0.277333 0.128-0.725333 0.128-1.28 0.256L437.781333 1.28c4.266667-0.704 8.576-1.258667 13.141334-1.258667z" fill="" p-id="2626"></path></svg>
|
After Width: | Height: | Size: 1.1 KiB |
1
client/packages/rtc-web/src/assets/svg-icon/up.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1691553973993" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2471" width="200" height="200"><path d="M896 467.2l-362.666667-341.333333c-2.133333-2.133333-6.4-4.266667-8.533333-6.4-4.266667-2.133333-6.4-2.133333-10.666667-2.133334s-8.533333 0-10.666666 2.133334c-4.266667 2.133333-6.4 4.266667-8.533334 6.4l-362.666666 341.333333c-12.8 12.8-12.8 32-2.133334 44.8 12.8 12.8 32 12.8 44.8 2.133333l309.333334-290.133333V874.666667c0 17.066667 14.933333 32 32 32s32-14.933333 32-32V224L853.333333 514.133333c6.4 6.4 14.933333 8.533333 21.333334 8.533334 8.533333 0 17.066667-4.266667 23.466666-10.666667 12.8-12.8 10.666667-32-2.133333-44.8z" fill="" p-id="2472"></path></svg>
|
After Width: | Height: | Size: 725 B |
@@ -0,0 +1 @@
|
||||
<svg t="1690124637853" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1266" width="200" height="200"><path d="M512 512m-511.982387 0a511.982387 511.982387 0 1 0 1023.964774 0 511.982387 511.982387 0 1 0-1023.964774 0Z" fill="#F9C228" p-id="1267"></path><path d="M917.619539 199.639491C761.536154-3.082215 483.485105-56.554248 265.951152 62.930031a531.620502 531.620502 0 0 0-166.598142 387.161472c0 294.430822 238.686756 533.099966 533.099966 533.099966a535.424837 535.424837 0 0 0 101.219951-9.598899 514.289646 514.289646 0 0 0 90.705195-55.973031C1048.393533 745.138768 1090.223598 423.672515 917.619539 199.639491z" fill="#FCDC22" p-id="1268"></path><path d="M972.042656 550.272308c-111.329618 114.904988-252.600757 198.670795-415.482628 260.033299 0 0 55.867355 156.664603 207.829377 101.53698 250.628139-90.846096 207.653251-361.570279 207.653251-361.570279z" fill="#FC9B88" p-id="1269"></path><path d="M522.690884 570.08655a80.119986 64.145304 90 1 0 128.290609 0 80.119986 64.145304 90 1 0-128.290609 0Z" fill="#282828" p-id="1270"></path><path d="M860.519298 429.449467a80.119986 54.881046 90 1 0 109.762092 0 80.119986 54.881046 90 1 0-109.762092 0Z" fill="#282828" p-id="1271"></path><path d="M953.813553 724.584795a261.319023 261.319023 0 0 0-116.014585-59.988717C754.121225 723.616099 659.699759 771.434469 556.560028 810.305607c0 0 55.867355 156.664603 207.829377 101.53698 110.959752-40.209701 164.361335-115.64472 189.424148-187.257792z" fill="#EA0F1A" p-id="1272"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
defineOptions({
|
||||
name: 'BackPreviousLevel',
|
||||
});
|
||||
|
||||
const emits = defineEmits(['back']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
class="btn-circle btn border-0 bg-transparent"
|
||||
@click="emits('back')"
|
||||
>
|
||||
<svg-icon name="back" class="h-6 w-6" />
|
||||
</button>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
26
client/packages/rtc-web/src/components/back/back-title.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'BackTitle',
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
svgName: {
|
||||
type: String as PropType<string>,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String as PropType<string>,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<!-- <svg-icon :name="props.svgName" class="mr-1 h-8 w-8"></svg-icon> -->
|
||||
<div v-if="props.title">{{ props.title }}</div>
|
||||
<slot v-else></slot>
|
||||
</div>
|
||||
</template>
|
2
client/packages/rtc-web/src/components/back/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as BackPreviousLevel } from './back-previous-level.vue';
|
||||
export { default as BackTitle } from './back-title.vue';
|
34
client/packages/rtc-web/src/components/base/dropdown.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { PropType } from 'vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'DropDown',
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['update:visible']);
|
||||
|
||||
const visibleVm = useVModel(props, 'visible', emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dropdown-top dropdown-end dropdown">
|
||||
<label tabindex="0">
|
||||
<slot></slot>
|
||||
</label>
|
||||
<div
|
||||
v-if="visibleVm"
|
||||
tabindex="0"
|
||||
class="dropdown-content menu rounded-box z-[1] bg-base-100 shadow"
|
||||
>
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@@ -1,3 +1,5 @@
|
||||
export { default as SvgIcon } from './svg-icon.vue';
|
||||
export { default as NavIcons } from './nav-icons.vue';
|
||||
export { default as Modal } from './modal.vue';
|
||||
export { default as Dropdown } from './dropdown.vue';
|
||||
export { default as ToastBox } from './toast.vue';
|
||||
|
@@ -1,11 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import { useSwitchTheme, ThemeEnum } from '@/hooks';
|
||||
import { computed } from 'vue';
|
||||
import { useSocketCount } from '@/hooks';
|
||||
|
||||
defineOptions({
|
||||
name: 'NavIcons',
|
||||
});
|
||||
|
||||
// 获取 当前在线人数
|
||||
const { data: curCount } = useSocketCount();
|
||||
|
||||
const { themeInfo, setTheme, isDark } = useSwitchTheme();
|
||||
|
||||
const themeIcon = computed(() =>
|
||||
@@ -18,9 +22,16 @@ const switchTheme = () =>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex items-center text-sm">
|
||||
<svg-icon name="count" class="mr-2 h-3 w-3" />
|
||||
在线人数:
|
||||
<div class="font-semibold">
|
||||
{{ curCount }}
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="https://github.com/tl-open-source/tl-rtc-file"
|
||||
class="cursor-pointer"
|
||||
class="ml-8 cursor-pointer"
|
||||
>
|
||||
<svg-icon
|
||||
name="github"
|
||||
|
73
client/packages/rtc-web/src/components/base/toast.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script lang="ts" setup>
|
||||
import { useVModel } from '@vueuse/core';
|
||||
|
||||
defineOptions({
|
||||
name: 'ToastBox',
|
||||
});
|
||||
|
||||
type ToastTypes = 'info' | 'success' | 'warning' | 'error';
|
||||
|
||||
interface ToastProps {
|
||||
msg?: string;
|
||||
type?: ToastTypes;
|
||||
closeable?: boolean;
|
||||
visible: boolean;
|
||||
horizontal?: 'left' | 'end' | 'center';
|
||||
vertical?: 'top' | 'middle' | 'bottom';
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ToastProps>(), {
|
||||
msg: '',
|
||||
type: 'info',
|
||||
closeable: false,
|
||||
visible: false,
|
||||
horizontal: 'left',
|
||||
vertical: 'top',
|
||||
});
|
||||
|
||||
const emits = defineEmits(['update:visible']);
|
||||
|
||||
const toastType = {
|
||||
info: 'alert-info',
|
||||
success: 'alert-success',
|
||||
warning: 'alert-warning',
|
||||
error: 'alert-error',
|
||||
};
|
||||
|
||||
const horizontalType = {
|
||||
left: 'toast-start',
|
||||
end: 'toast-end',
|
||||
center: 'toast-center',
|
||||
};
|
||||
|
||||
const verticalType = {
|
||||
top: 'toast-top',
|
||||
middle: 'toast-middle',
|
||||
bottom: 'toast-bottom',
|
||||
};
|
||||
|
||||
const visiblevm = useVModel(props, 'visible', emits);
|
||||
|
||||
setTimeout(() => {
|
||||
visiblevm.value = false;
|
||||
}, 1500);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="visiblevm"
|
||||
class="toast"
|
||||
:class="[horizontalType[props.horizontal], verticalType[props.vertical]]"
|
||||
>
|
||||
<div class="alert relative" :class="toastType[props.type]">
|
||||
<button
|
||||
v-if="props.closeable"
|
||||
class="btn-ghost btn-xs btn-circle btn absolute right-1 top-1"
|
||||
@click="visiblevm = false"
|
||||
>
|
||||
x
|
||||
</button>
|
||||
<span>{{ props.msg }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
import ChatMessage from './chat-message.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ChatContent',
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
msgList: {
|
||||
time: string;
|
||||
username: string;
|
||||
message: string;
|
||||
reverse: boolean;
|
||||
}[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col justify-end overflow-y-auto overflow-x-hidden">
|
||||
<ChatMessage
|
||||
v-for="(item, index) in props.msgList"
|
||||
:key="index"
|
||||
class="mb-4"
|
||||
:time="item.time"
|
||||
:username="item.username"
|
||||
:message="item.message"
|
||||
:reverse="item.reverse"
|
||||
:background="item.reverse ? 'bg-success' : ''"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,54 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
|
||||
defineOptions({
|
||||
name: 'ChatInput',
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
msg: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['sendMsg', 'update:msg']);
|
||||
|
||||
const msg = useVModel(props, 'msg', emits);
|
||||
|
||||
const canSend = computed(() => msg.value !== '');
|
||||
|
||||
const handleSend = () => {
|
||||
if (canSend.value) {
|
||||
emits('sendMsg', msg.value);
|
||||
msg.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const keyDownEvent = (e: any) => {
|
||||
if (e.ctrlKey && e.keyCode === 13) {
|
||||
handleSend();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col rounded-lg border border-info">
|
||||
<textarea
|
||||
v-model="msg"
|
||||
class="textarea flex-1 resize-none border-b-0 focus:outline-none"
|
||||
placeholder="请输入聊天消息 使用 Ctrl + Enter 发送"
|
||||
@keydown="keyDownEvent"
|
||||
></textarea>
|
||||
<div class="mb-2 flex justify-end pr-4">
|
||||
<button
|
||||
class="btn-info btn px-3"
|
||||
:class="{ 'btn-disabled': !canSend }"
|
||||
@click="handleSend"
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import UserCard from './user-card.vue';
|
||||
import { UserCardProps } from './props';
|
||||
|
||||
type ChatMessageProps = Partial<
|
||||
UserCardProps & {
|
||||
message: string;
|
||||
background: string;
|
||||
}
|
||||
>;
|
||||
|
||||
defineOptions({
|
||||
name: 'ChatMessage',
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<ChatMessageProps>(), {
|
||||
iconname: 'user-smail',
|
||||
iconcolor: 'currentcolor',
|
||||
message: '',
|
||||
background: '',
|
||||
reverse: false,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UserCard v-bind="props">
|
||||
<div
|
||||
class="flex flex-wrap"
|
||||
:class="[reverse ? 'justify-end' : 'justify-start']"
|
||||
>
|
||||
<div
|
||||
class="max-w-[260px] whitespace-pre-wrap break-words rounded-lg px-4 py-2 leading-6 text-black lg:max-w-[500px]"
|
||||
:class="[background ? background : 'bg-slate-200']"
|
||||
>
|
||||
{{ props.message }}
|
||||
</div>
|
||||
</div>
|
||||
</UserCard>
|
||||
</template>
|
@@ -0,0 +1,86 @@
|
||||
<script lang="ts" setup>
|
||||
import { Member, useDragChangeSize } from '@/hooks';
|
||||
import UserCard from './user-card.vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { PropType } from 'vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ChatRoomUsers',
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
members: {
|
||||
type: Array as PropType<Partial<Member>[]>,
|
||||
default: () => [],
|
||||
},
|
||||
roomOwner: {
|
||||
type: Object as PropType<Partial<Member>>,
|
||||
default: () => ({}),
|
||||
},
|
||||
self: {
|
||||
type: Object as PropType<Partial<Member>>,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String as PropType<string>,
|
||||
default: '220px',
|
||||
},
|
||||
});
|
||||
|
||||
const memberwithoutOwner = computed(() =>
|
||||
props.members.filter((item) => !item.owner)
|
||||
);
|
||||
|
||||
const chatRoomUserRef = ref<any>();
|
||||
|
||||
const { style, canDragged } = useDragChangeSize(chatRoomUserRef, {
|
||||
initialSize: {
|
||||
width: props.width,
|
||||
},
|
||||
position: 'left',
|
||||
persistentSize: {
|
||||
height: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
const reSizeClass = computed(() => {
|
||||
if (canDragged.value.draggable) {
|
||||
if (canDragged.value.position === 'left') {
|
||||
return ['cursor-w-resize'];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="chatRoomUserRef"
|
||||
:class="[...reSizeClass]"
|
||||
class="h-full overflow-auto px-2 py-2 text-sm"
|
||||
:style="style"
|
||||
>
|
||||
<div class="mb-1 text-gray-400">房主</div>
|
||||
<UserCard
|
||||
class="mb-4"
|
||||
iconcolor="#2F54EB"
|
||||
:username="
|
||||
roomOwner?.id === self?.id
|
||||
? `${roomOwner?.nickName} (本人)`
|
||||
: roomOwner?.nickName
|
||||
"
|
||||
/>
|
||||
|
||||
<div class="mb-1 text-gray-400">
|
||||
房间成员 ({{ memberwithoutOwner.length }})
|
||||
</div>
|
||||
<UserCard
|
||||
v-for="item in memberwithoutOwner"
|
||||
:key="item.id"
|
||||
:username="
|
||||
item.id === self?.id ? `${item.nickName} (本人)` : item.nickName
|
||||
"
|
||||
iconname="user-smail"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,5 @@
|
||||
export { default as ChatInput } from './chat-input.vue';
|
||||
export { default as ChatMessage } from './chat-message.vue';
|
||||
export { default as ChatRoomUser } from './chat-room-user.vue';
|
||||
export { default as ChatContent } from './chat-content.vue';
|
||||
export { default as UserCard } from './user-card.vue';
|
@@ -0,0 +1,7 @@
|
||||
export type UserCardProps = {
|
||||
iconname?: string;
|
||||
iconcolor?: string;
|
||||
username?: string;
|
||||
time?: string;
|
||||
reverse?: boolean;
|
||||
};
|
@@ -0,0 +1,37 @@
|
||||
<script lang="ts" setup>
|
||||
import { UserCardProps } from './props';
|
||||
|
||||
defineOptions({
|
||||
name: 'UserCard',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
withDefaults(defineProps<UserCardProps>(), {
|
||||
iconname: 'member',
|
||||
iconcolor: 'currentcolor',
|
||||
username: '',
|
||||
time: '',
|
||||
reverse: false,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="user-card flex items-start"
|
||||
:class="[reverse ? 'flex-row-reverse' : '']"
|
||||
>
|
||||
<button class="btn-square btn border-0 bg-transparent">
|
||||
<svg-icon :name="iconname" class="h-8 w-8" :color="iconcolor" />
|
||||
</button>
|
||||
<div class="ml-1 flex flex-col text-sm">
|
||||
<div class="mb-1" :class="[reverse ? 'text-right' : 'text-left']">
|
||||
{{ username }}
|
||||
<span v-if="time" class="ml-4 text-slate-700 dark:text-gray-300">{{
|
||||
time
|
||||
}}</span>
|
||||
</div>
|
||||
<slot></slot>
|
||||
<!-- <div class="other-info"></div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
39
client/packages/rtc-web/src/components/emoji/index.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import Popper from 'vue3-popper';
|
||||
import EmojiPicker from 'vue3-emoji-picker';
|
||||
import 'vue3-emoji-picker/css';
|
||||
import { useTheme } from '@/hooks';
|
||||
import { ref } from 'vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'EmojiPicker',
|
||||
});
|
||||
|
||||
const emits = defineEmits(['emojiChange']);
|
||||
|
||||
const { isDark } = useTheme();
|
||||
|
||||
const visibleEmoji = ref(false);
|
||||
|
||||
const onSelectEmoji = (data: any) => {
|
||||
emits('emojiChange', data);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Popper
|
||||
@open:popper="visibleEmoji = true"
|
||||
@close:popper="visibleEmoji = false"
|
||||
>
|
||||
<slot></slot>
|
||||
|
||||
<template #content>
|
||||
<EmojiPicker
|
||||
v-if="visibleEmoji"
|
||||
:native="true"
|
||||
:theme="isDark ? 'dark' : 'light'"
|
||||
@select="onSelectEmoji"
|
||||
/>
|
||||
</template>
|
||||
</Popper>
|
||||
</template>
|
@@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ErrorBoundary',
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
tipsList: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full w-full items-center justify-center">
|
||||
<div v-if="props.tipsList.length">
|
||||
<p class="mb-2 font-bold">无法显示?也许可以帮到您:</p>
|
||||
<ul class="text-secondary">
|
||||
<li v-for="(item, index) in props.tipsList" :key="index">
|
||||
{{ index + 1 }}. {{ item }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
@@ -2,7 +2,6 @@
|
||||
import { useRoom } from '@/hooks';
|
||||
import { useForm } from '../form-base';
|
||||
import { computed } from 'vue';
|
||||
import { watch } from 'vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'FormRoom',
|
||||
@@ -16,13 +15,6 @@ const { validateRoomId } = useRoom(() => formData.value.roomId || '');
|
||||
|
||||
const roomIdValid = computed(() => !formErrors.value['roomId']);
|
||||
|
||||
watch(
|
||||
() => roomIdValid.value,
|
||||
(v) => {
|
||||
console.log(v);
|
||||
}
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
onSubmit: handleSubmit,
|
||||
resetForm: resetFields,
|
||||
|
@@ -1,8 +1,12 @@
|
||||
import { SetupContext } from 'vue';
|
||||
import { SvgIcon, NavIcons } from './base';
|
||||
import { MenuSide } from './menu';
|
||||
import { resetUrl } from '@/utils';
|
||||
|
||||
export const NavHeader = () => {
|
||||
export const NavHeader = (
|
||||
{ showSiderbar }: Partial<{ showSiderbar: boolean }>,
|
||||
ctx: SetupContext
|
||||
) => {
|
||||
return (
|
||||
<div class="drawer shadow-md dark:shadow-sm dark:shadow-neutral-600">
|
||||
<input id="my-drawer-3" type="checkbox" class="drawer-toggle" />
|
||||
@@ -10,22 +14,39 @@ export const NavHeader = () => {
|
||||
{/* Navbar */}
|
||||
<div class="navbar w-full">
|
||||
<div class="flex-none lg:hidden">
|
||||
<label for="my-drawer-3" class="btn-ghost btn-square btn">
|
||||
<SvgIcon
|
||||
name="nav-menu"
|
||||
color="#A6ADBA"
|
||||
class="inline-block h-6 w-6"
|
||||
/>
|
||||
</label>
|
||||
{showSiderbar ? (
|
||||
<label
|
||||
for="my-drawer-3"
|
||||
class="btn-ghost btn-square btn"
|
||||
onClick={() => ctx.emit('toggle')}
|
||||
>
|
||||
<SvgIcon
|
||||
name="nav-menu"
|
||||
color="#A6ADBA"
|
||||
class="inline-block h-6 w-6"
|
||||
/>
|
||||
</label>
|
||||
) : undefined}
|
||||
</div>
|
||||
<div class="mx-2 flex-1 px-2">
|
||||
<span class="cursor-pointer" onClick={resetUrl}>
|
||||
Web-Rtc
|
||||
</span>
|
||||
</div>
|
||||
<div class="mx-2 flex-1 px-2">Web-Rtc</div>
|
||||
<NavIcons class="flex-none pr-3" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="drawer-side">
|
||||
<label for="my-drawer-3" class="drawer-overlay"></label>
|
||||
<MenuSide class="bg-base-200"></MenuSide>
|
||||
</div>
|
||||
|
||||
{showSiderbar ? (
|
||||
<div class="drawer-side z-50">
|
||||
<label
|
||||
for="my-drawer-3"
|
||||
class="drawer-overlay"
|
||||
onClick={() => ctx.emit('toggle')}
|
||||
></label>
|
||||
<MenuSide class="bg-base-200"></MenuSide>
|
||||
</div>
|
||||
) : undefined}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -47,7 +68,7 @@ export const FullHeightFlexBox = (
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={['flex', height[type], `flex-${dire}`]}>
|
||||
<div class={['flex', height[type], `flex-${dire}`, 'min-h-0']}>
|
||||
{ctx.slots.default?.()}
|
||||
</div>
|
||||
);
|
||||
|
40
client/packages/rtc-web/src/components/list/select-list.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script lang="ts" setup generic="T">
|
||||
defineOptions({
|
||||
name: 'SelectList',
|
||||
});
|
||||
|
||||
type SelectListPropsType<L> = {
|
||||
list: L[];
|
||||
uniKey?: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
const emits = defineEmits(['selected']);
|
||||
|
||||
const props = withDefaults(defineProps<SelectListPropsType<T>>(), {
|
||||
list: () => [] as T[],
|
||||
uniKey: '',
|
||||
title: '',
|
||||
});
|
||||
|
||||
const handleSelected = (item: any) => {
|
||||
emits('selected', item);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col pb-1">
|
||||
<div v-if="props.title" class="list-title select-none px-2 text-slate-500">
|
||||
{{ props.title }}
|
||||
</div>
|
||||
<ul class="flex flex-col">
|
||||
<li
|
||||
v-for="(item, index) in props.list"
|
||||
:key="props.uniKey ? (item as any)[props.uniKey] : index"
|
||||
@click="handleSelected(item)"
|
||||
>
|
||||
<slot :item="item"></slot>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
75
client/packages/rtc-web/src/components/menu-action.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script lang="ts" setup>
|
||||
import EmojiPicker from '@/components/emoji/index.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'MenuActions',
|
||||
});
|
||||
|
||||
type PropTypes = {
|
||||
menuAction: {
|
||||
name: string;
|
||||
tip: string;
|
||||
color?: string;
|
||||
tipDir?: string;
|
||||
btn?: boolean;
|
||||
disabled?: boolean;
|
||||
}[];
|
||||
gap?: number;
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<PropTypes>(), {
|
||||
menuAction: () => [] as PropTypes['menuAction'],
|
||||
gap: 0,
|
||||
});
|
||||
|
||||
const emits = defineEmits(['clickIcon']);
|
||||
|
||||
const handleClick = (name: string) => emits('clickIcon', name);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex">
|
||||
<div
|
||||
v-for="(item, index) in props.menuAction"
|
||||
:key="item.name"
|
||||
:data-tip="item.tip"
|
||||
class="tooltip"
|
||||
:class="[item.tipDir ?? 'tooltip-bottom', props.gap ? 'first:ml-0' : '']"
|
||||
:style="
|
||||
props.gap && index === 0
|
||||
? `margin-left: 0`
|
||||
: props.gap
|
||||
? `margin-left: ${props.gap}px`
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<emoji-picker v-if="item.name === 'emoji'" v-bind="$attrs">
|
||||
<svg-icon
|
||||
:color="item.color"
|
||||
:name="item.name"
|
||||
class="h-6 w-6 cursor-pointer"
|
||||
@click="handleClick(item.name)"
|
||||
/>
|
||||
</emoji-picker>
|
||||
<button
|
||||
v-else-if="item.btn"
|
||||
class="btn-circle btn"
|
||||
:disabled="item.disabled"
|
||||
@click="handleClick(item.name)"
|
||||
>
|
||||
<svg-icon
|
||||
:color="item.color"
|
||||
:name="item.name"
|
||||
class="h-6 w-6 cursor-pointer"
|
||||
/>
|
||||
</button>
|
||||
<svg-icon
|
||||
v-else
|
||||
:color="item.color"
|
||||
:name="item.name"
|
||||
class="h-6 w-6 cursor-pointer"
|
||||
@click="handleClick(item.name)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@@ -20,6 +20,11 @@ const menuListData = ref([
|
||||
icon: 'add-icon',
|
||||
label: '聊天',
|
||||
},
|
||||
{
|
||||
key: 'video',
|
||||
icon: 'add-icon',
|
||||
label: '视频聊天',
|
||||
},
|
||||
]);
|
||||
|
||||
const currentCreateRoomByKey = ref('');
|
||||
@@ -56,7 +61,10 @@ const handleClose = () => {
|
||||
class="flex items-center justify-between"
|
||||
>
|
||||
<div>{{ item.label }}</div>
|
||||
<button class="btn-circle btn bg-transparent" @click="createRoom(item.key)">
|
||||
<button
|
||||
class="btn-circle btn border-0 bg-transparent"
|
||||
@click="createRoom(item.key)"
|
||||
>
|
||||
<svg-icon :name="item.icon" class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -7,7 +7,7 @@ defineOptions({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="menu h-full w-80 p-4">
|
||||
<div class="menu h-full w-80 min-w-[320px] p-4">
|
||||
<MenuList />
|
||||
</div>
|
||||
</template>
|
||||
|
3
client/packages/rtc-web/src/config/config-enum.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const enum ConfigEnum {
|
||||
useRelay = 'tl-rtc-file-use-relay',
|
||||
}
|
53
client/packages/rtc-web/src/config/constant.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export type MenuActionType = {
|
||||
name: string;
|
||||
tip: string;
|
||||
color?: string;
|
||||
tipDir?: string;
|
||||
btn?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const ChatAction: MenuActionType[] = [
|
||||
{ name: 'member', tip: '显示成员', color: undefined },
|
||||
];
|
||||
|
||||
export const ChatInputAction: MenuActionType[] = [
|
||||
{ name: 'emoji', tip: '表情' },
|
||||
];
|
||||
|
||||
export const VideoMenuAction: MenuActionType[] = [
|
||||
{
|
||||
name: 'member',
|
||||
tip: '显示成员',
|
||||
color: undefined,
|
||||
tipDir: 'tooltip-top',
|
||||
btn: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const VideoControlMenuAction: MenuActionType[] = [
|
||||
{
|
||||
name: 'camera',
|
||||
tip: '开启/关闭摄像头',
|
||||
color: '#707070',
|
||||
tipDir: 'tooltip-top',
|
||||
btn: true,
|
||||
disabled: false,
|
||||
},
|
||||
// {
|
||||
// name: 'mirror-image',
|
||||
// tip: '开启镜像',
|
||||
// color: '#707070',
|
||||
// tipDir: 'tooltip-top',
|
||||
// btn: true,
|
||||
// disabled: false,
|
||||
// },
|
||||
{
|
||||
name: 'hang-up',
|
||||
tip: '结束通话',
|
||||
color: undefined,
|
||||
tipDir: 'tooltip-top',
|
||||
btn: true,
|
||||
disabled: false,
|
||||
},
|
||||
];
|
3
client/packages/rtc-web/src/config/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './config-enum';
|
||||
export * from './constant';
|
||||
export * from './socket-event-name';
|
15
client/packages/rtc-web/src/config/socket-event-name.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export const enum SocketEventName {
|
||||
Count = 'count',
|
||||
CreateAndJoin = 'createAndJoin',
|
||||
RoomCreated = 'created',
|
||||
RoomExit = 'exit',
|
||||
RoomJoin = 'joined',
|
||||
RoomOffer = 'offer',
|
||||
RoomAnswer = 'answer',
|
||||
RoomCandidate = 'candidate',
|
||||
}
|
||||
|
||||
// chat 的事件名
|
||||
export const enum ChatEventName {
|
||||
ChatingRoom = 'chatingRoom',
|
||||
}
|
15
client/packages/rtc-web/src/context/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { InjectionKey, Ref } from 'vue';
|
||||
|
||||
export type InitDataKeyType = Partial<{
|
||||
langMode: string;
|
||||
socket: any;
|
||||
logo: string;
|
||||
version: string;
|
||||
options: RTCOfferOptions;
|
||||
config: {
|
||||
iceServers: RTCIceServer[];
|
||||
};
|
||||
}>;
|
||||
|
||||
export const InitDataKey: InjectionKey<Ref<InitDataKeyType>> =
|
||||
Symbol('initDataKey');
|
2
client/packages/rtc-web/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
declare module 'socket.io-client';
|
||||
declare module 'vue3-emoji-picker';
|
@@ -1,2 +1,9 @@
|
||||
export * from './useTheme';
|
||||
export * from './useRoom';
|
||||
export * from './useInitData';
|
||||
export * from './socket-utils';
|
||||
export * from './useRouterReactive';
|
||||
export * from './useDragChangeSize';
|
||||
export * from './useSwitchByUrl';
|
||||
export * from './useErrorCaptured';
|
||||
export * from './useUseragent';
|
||||
|
2
client/packages/rtc-web/src/hooks/socket-utils/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './useSocket';
|
||||
export * from './useSocketUtils';
|
21
client/packages/rtc-web/src/hooks/socket-utils/useSocket.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { InitDataKey } from '@/context';
|
||||
import { CommonFnType } from '@/types';
|
||||
import { inject, watch } from 'vue';
|
||||
|
||||
export const useSocket = (fn?: CommonFnType) => {
|
||||
const initData = inject(InitDataKey);
|
||||
|
||||
if (initData) {
|
||||
watch(
|
||||
() => initData.value.socket,
|
||||
async (v) => {
|
||||
if (v) {
|
||||
await fn?.(v);
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
@@ -0,0 +1,18 @@
|
||||
import { SocketEventName } from '@/config';
|
||||
import { useSocket } from './useSocket';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export const useSocketCount = () => {
|
||||
const count = ref(0);
|
||||
const handleSocketCount = (socket: any) => {
|
||||
socket.on(SocketEventName.Count, (data: any) => {
|
||||
count.value = data.mc;
|
||||
});
|
||||
};
|
||||
|
||||
useSocket(handleSocketCount);
|
||||
|
||||
return {
|
||||
data: count,
|
||||
};
|
||||
};
|
128
client/packages/rtc-web/src/hooks/useDragChangeSize.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @description 拖拽元素改变大小
|
||||
* 对于一些自由拖拽的元素会有一些问题,比如拉左边进行宽度增加,会造成 width 往右边增加;这里暂时未处理,按理说要同时改变 left
|
||||
*/
|
||||
|
||||
import { isItBetween } from '@/utils';
|
||||
import {
|
||||
MaybeElementRef,
|
||||
useMouseInElement,
|
||||
useMousePressed,
|
||||
} from '@vueuse/core';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
type ResizeOption = {
|
||||
initialSize?: {
|
||||
width?: string;
|
||||
height?: string;
|
||||
};
|
||||
position?: 'left' | 'right' | 'top' | 'bottom';
|
||||
persistentSize?: {
|
||||
width?: string;
|
||||
height?: string;
|
||||
}; // 用来控制在拖拽时不会变的数据
|
||||
};
|
||||
|
||||
export const useDragChangeSize = (
|
||||
target: MaybeElementRef,
|
||||
options?: ResizeOption
|
||||
) => {
|
||||
const offset = 5;
|
||||
const { elementX, elementY, elementWidth, elementHeight } =
|
||||
useMouseInElement(target);
|
||||
|
||||
const style = computed(() => {
|
||||
return Object.assign(
|
||||
{},
|
||||
{
|
||||
width: `${
|
||||
elementWidth.value
|
||||
? elementWidth.value + 'px'
|
||||
: options?.initialSize?.width ?? '100%'
|
||||
}`,
|
||||
height: `${
|
||||
elementHeight.value
|
||||
? elementHeight.value + 'px'
|
||||
: options?.initialSize?.height ?? '100%'
|
||||
}`,
|
||||
},
|
||||
options?.persistentSize || ({} as any)
|
||||
);
|
||||
});
|
||||
|
||||
const { pressed } = useMousePressed({ target, touch: false });
|
||||
|
||||
const draggablePos = computed(() => ({
|
||||
left: {
|
||||
x: [-offset, offset],
|
||||
y: [0, elementHeight.value],
|
||||
},
|
||||
top: {
|
||||
x: [0, elementWidth.value],
|
||||
y: [-offset, offset],
|
||||
},
|
||||
}));
|
||||
|
||||
const judgeEnterPos = () => {
|
||||
const findV = Object.keys(draggablePos.value).find((key) => {
|
||||
const posKey = key as keyof typeof draggablePos.value;
|
||||
|
||||
return (
|
||||
isItBetween(elementX.value, draggablePos.value[posKey].x) &&
|
||||
isItBetween(elementY.value, draggablePos.value[posKey].y)
|
||||
);
|
||||
});
|
||||
const draggable = options?.position ? options.position === findV : !!findV;
|
||||
return {
|
||||
draggable,
|
||||
position: findV || '',
|
||||
};
|
||||
};
|
||||
|
||||
// 判断点击时候是否在拖拽范围内
|
||||
const draggableByClick = ref({
|
||||
draggable: false,
|
||||
position: '',
|
||||
});
|
||||
|
||||
// 供外部用的判断 是否可以拖拽
|
||||
const canDragged = computed(judgeEnterPos);
|
||||
|
||||
const doResize = ([posX, posY]: number[], type: string) => {
|
||||
const movX = posX - offset;
|
||||
const movY = posY - offset;
|
||||
if (type === 'left') {
|
||||
const differenceX = -movX;
|
||||
elementWidth.value = elementWidth.value + differenceX;
|
||||
}
|
||||
|
||||
if (type === 'top') {
|
||||
const differenceY = -movY;
|
||||
elementHeight.value = elementHeight.value + differenceY;
|
||||
}
|
||||
};
|
||||
|
||||
watch([elementX, elementY], (v) => {
|
||||
if (pressed.value) {
|
||||
if (draggableByClick.value.draggable) {
|
||||
doResize(v, draggableByClick.value.position);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watch(pressed, (v) => {
|
||||
if (v) {
|
||||
draggableByClick.value = judgeEnterPos();
|
||||
} else {
|
||||
draggableByClick.value = {
|
||||
draggable: false,
|
||||
position: '',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
canDragged,
|
||||
style,
|
||||
};
|
||||
};
|
12
client/packages/rtc-web/src/hooks/useErrorCaptured.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { onErrorCaptured, ref } from 'vue';
|
||||
|
||||
// 捕获 组件error
|
||||
export const useErrorCaptured = () => {
|
||||
const errors = ref<string[]>([]);
|
||||
|
||||
onErrorCaptured((e) => {
|
||||
errors.value.push(e.message);
|
||||
});
|
||||
|
||||
return errors;
|
||||
};
|
47
client/packages/rtc-web/src/hooks/useInitData.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { ConfigEnum } from '@/config';
|
||||
import { useFetch, useLocalStorage } from '@vueuse/core';
|
||||
import { ref, shallowReactive } from 'vue';
|
||||
import io from 'socket.io-client';
|
||||
import { InitDataKeyType } from '@/context';
|
||||
import { isDev } from '@/utils';
|
||||
|
||||
export const useFetchData = async () => {
|
||||
const useTurn = useLocalStorage(ConfigEnum.useRelay, isDev() ? false : true);
|
||||
|
||||
const { data, error } = await useFetch(
|
||||
() => `/api/comm/initData?turn=${useTurn.value}`
|
||||
)
|
||||
.get()
|
||||
.json();
|
||||
|
||||
return { data, error };
|
||||
};
|
||||
|
||||
export const useInitData = async () => {
|
||||
const { data, error } = await useFetchData();
|
||||
const initData = ref<InitDataKeyType>({
|
||||
langMode: 'zh', // 默认中文
|
||||
});
|
||||
|
||||
if (data.value) {
|
||||
const { wsHost, logo, version, rtcConfig, options } = data.value;
|
||||
initData.value = Object.assign({}, initData.value, {
|
||||
socket: wsHost ? shallowReactive(io(wsHost)) : null,
|
||||
logo,
|
||||
version,
|
||||
options: Object.keys(options).reduce(
|
||||
(cur, next) => ({ ...cur, [next]: Boolean(options[next]) }),
|
||||
{}
|
||||
),
|
||||
config: rtcConfig,
|
||||
});
|
||||
}
|
||||
|
||||
if (error.value) {
|
||||
throw error.value;
|
||||
}
|
||||
|
||||
return {
|
||||
initData,
|
||||
};
|
||||
};
|
@@ -1,6 +1,53 @@
|
||||
import { CommonFnType } from '@/types';
|
||||
import { resolveRef } from '@/utils/reactive';
|
||||
import { MaybeRef, computed } from 'vue';
|
||||
import {
|
||||
MaybeRef,
|
||||
computed,
|
||||
inject,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
shallowRef,
|
||||
watch,
|
||||
} from 'vue';
|
||||
import { useSocket } from './socket-utils';
|
||||
import { SocketEventName } from '@/config';
|
||||
import { useRouteParamsReactive, useUserAgent } from '.';
|
||||
import { InitDataKey } from '@/context';
|
||||
import { genNickName, isDev, resetUrl } from '@/utils';
|
||||
import { uniqBy } from 'lodash';
|
||||
import { watchArray } from '@vueuse/core';
|
||||
|
||||
export type Member = {
|
||||
id: string;
|
||||
nickName: string;
|
||||
langMode: string;
|
||||
owner: boolean;
|
||||
ua: string;
|
||||
joinTime: string;
|
||||
userAgent: string;
|
||||
ip: string;
|
||||
network: string;
|
||||
room?: string;
|
||||
roomInfo?: OwnerInfo;
|
||||
};
|
||||
|
||||
export type OwnerInfo = {
|
||||
socketId: string;
|
||||
roomId: string;
|
||||
recoderId: string;
|
||||
owner: boolean;
|
||||
};
|
||||
|
||||
export type ConnectOption = {
|
||||
roomJoined?: (...args: any[]) => Promise<void>;
|
||||
roomCreated?: (data: any) => Promise<void>;
|
||||
onAddRtcPeer?: (id: string, pc: any) => Promise<void>;
|
||||
onConnectComplete?: (...args: any[]) => void;
|
||||
onBeforeCreateOffer?: (...args: any[]) => Promise<void>;
|
||||
onBeforeCreateAnswer?: (...args: any[]) => Promise<void>;
|
||||
onTrack?: (...args: any[]) => void;
|
||||
};
|
||||
|
||||
export const useRoom = (value: MaybeRef<string> | CommonFnType) => {
|
||||
const realValue = resolveRef(value);
|
||||
@@ -17,3 +64,317 @@ export const useRoom = (value: MaybeRef<string> | CommonFnType) => {
|
||||
isValid,
|
||||
};
|
||||
};
|
||||
|
||||
export const useCreateRoom = (
|
||||
type: 'password' | 'video' = 'password',
|
||||
validate = true
|
||||
) => {
|
||||
const initData = inject(InitDataKey);
|
||||
|
||||
const { getNetWorkState, isMobile } = useUserAgent();
|
||||
|
||||
const { roomId } = useRouteParamsReactive(['roomId']);
|
||||
|
||||
const { isValid } = useRoom(() => roomId.value || '');
|
||||
|
||||
const emitCreateRoom = (socket: any) => {
|
||||
socket.emit(SocketEventName.CreateAndJoin, {
|
||||
room: roomId.value,
|
||||
type: type,
|
||||
password: '',
|
||||
nickName: genNickName(),
|
||||
langMode: initData?.value.langMode,
|
||||
ua: isMobile ? 'mobile' : 'pc',
|
||||
network: getNetWorkState(),
|
||||
});
|
||||
};
|
||||
|
||||
const checkParams = () => {
|
||||
console.log('check');
|
||||
if (validate) {
|
||||
if (!isValid.value) {
|
||||
resetUrl();
|
||||
} else {
|
||||
useSocket(emitCreateRoom);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => isValid.value,
|
||||
() => {
|
||||
checkParams();
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(checkParams);
|
||||
|
||||
return {
|
||||
roomId,
|
||||
isValid,
|
||||
};
|
||||
};
|
||||
|
||||
export const useRoomConnect = (option: ConnectOption = {}) => {
|
||||
const roomInfo = useGetRoomInfo();
|
||||
|
||||
const { members, selfInfo } = roomInfo;
|
||||
const rtcConnects = new Map<string, RTCPeerConnection>();
|
||||
const dataChanelMap = new Map<string, RTCDataChannel>();
|
||||
const initData = inject(InitDataKey)!;
|
||||
const socketRef = shallowRef();
|
||||
const completed = ref(false);
|
||||
|
||||
watchArray(
|
||||
() => members.value,
|
||||
async (_, __, added, removed) => {
|
||||
console.log('watch', added, removed);
|
||||
|
||||
// 处理 exit
|
||||
if (removed.length) {
|
||||
removed.forEach(async (peer) => {
|
||||
if (peer.id) {
|
||||
const rtcConnect = await getRtcConnect(peer.id);
|
||||
rtcConnect.close();
|
||||
rtcConnects.delete(peer.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const createRtcConnect = async (id: string) => {
|
||||
const pc = new RTCPeerConnection(
|
||||
isDev() ? undefined : initData.value.config
|
||||
);
|
||||
pc.onicecandidate = (event) => {
|
||||
if (event.candidate != null) {
|
||||
const message = {
|
||||
from: selfInfo.value.socketId,
|
||||
to: id,
|
||||
room: selfInfo.value.roomId,
|
||||
sdpMid: event.candidate.sdpMid,
|
||||
sdpMLineIndex: event.candidate.sdpMLineIndex,
|
||||
sdp: event.candidate.candidate,
|
||||
};
|
||||
socketRef.value.emit('candidate', message);
|
||||
}
|
||||
};
|
||||
|
||||
const dataChanel = pc.createDataChannel('datachanel');
|
||||
dataChanelMap.set(id, dataChanel);
|
||||
|
||||
dataChanel.onopen = () => {
|
||||
// dataChanel.send('aaa');
|
||||
};
|
||||
|
||||
pc.ondatachannel = (e) => {
|
||||
const chanel = e.channel;
|
||||
if (chanel.label === 'datachanel') {
|
||||
chanel.onmessage = () => {
|
||||
// console.log('接受', e.data);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pc.onconnectionstatechange = () => {
|
||||
if (pc.connectionState === 'connected') {
|
||||
// console.log('完成');
|
||||
completed.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
pc.ontrack = (e) => {
|
||||
option?.onTrack?.(e, id);
|
||||
};
|
||||
|
||||
rtcConnects.set(id, pc);
|
||||
await option.onAddRtcPeer?.(id, pc);
|
||||
|
||||
return pc;
|
||||
};
|
||||
|
||||
const getRtcConnect = async (id: string) => {
|
||||
return rtcConnects.get(id) || (await createRtcConnect(id));
|
||||
};
|
||||
|
||||
const roomCreated = async (data: any) => {
|
||||
await option?.roomCreated?.(data);
|
||||
};
|
||||
|
||||
const roomJoined = async (data: any) => {
|
||||
const peer = await getRtcConnect(data.id);
|
||||
|
||||
await option?.roomJoined?.(data.id, peer);
|
||||
createOffer(peer, data);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 这里的 createOffer
|
||||
*/
|
||||
const createOffer = async (pc: RTCPeerConnection, peer: any) => {
|
||||
await option?.onBeforeCreateOffer?.(peer.id, pc);
|
||||
const offer = await pc.createOffer(initData.value.options);
|
||||
await pc.setLocalDescription(offer);
|
||||
console.log('create offer - send');
|
||||
socketRef.value.emit(SocketEventName.RoomOffer, {
|
||||
from: selfInfo.value.socketId,
|
||||
to: peer.id,
|
||||
room: selfInfo.value.roomId,
|
||||
sdp: offer.sdp,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description offer 监听事件
|
||||
*/
|
||||
const roomOffer = async (data: any) => {
|
||||
const pc = await getRtcConnect(data.from);
|
||||
await pc.setRemoteDescription(
|
||||
new RTCSessionDescription({ type: 'offer', sdp: data.sdp })
|
||||
);
|
||||
await option?.onBeforeCreateAnswer?.(data.from, pc);
|
||||
const answer = await pc.createAnswer(initData.value.options);
|
||||
await pc.setLocalDescription(answer);
|
||||
console.log('receive offer - send answer');
|
||||
socketRef.value.emit(SocketEventName.RoomAnswer, {
|
||||
from: selfInfo.value.socketId,
|
||||
to: data.from,
|
||||
room: selfInfo.value.roomId,
|
||||
sdp: answer.sdp,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description answer 监听事件
|
||||
*/
|
||||
const roomAnswer = async (data: any) => {
|
||||
const pc = await getRtcConnect(data.from);
|
||||
console.log('receive answer');
|
||||
await pc.setRemoteDescription(
|
||||
new RTCSessionDescription({ type: 'answer', sdp: data.sdp })
|
||||
);
|
||||
};
|
||||
const roomCandidate = async (data: any) => {
|
||||
const pc = await getRtcConnect(data.from);
|
||||
const rtcIceCandidate = new RTCIceCandidate({
|
||||
candidate: data.sdp,
|
||||
sdpMid: data.sdpMid,
|
||||
sdpMLineIndex: data.sdpMLineIndex,
|
||||
});
|
||||
await pc.addIceCandidate(rtcIceCandidate);
|
||||
};
|
||||
|
||||
const handleRoomConnect = (socket: any) => {
|
||||
socketRef.value = socket;
|
||||
startConnect();
|
||||
};
|
||||
|
||||
const startConnect = () => {
|
||||
socketRef.value.on(SocketEventName.RoomCreated, roomCreated);
|
||||
socketRef.value.on(SocketEventName.RoomJoin, roomJoined);
|
||||
socketRef.value.on(SocketEventName.RoomOffer, roomOffer);
|
||||
socketRef.value.on(SocketEventName.RoomAnswer, roomAnswer);
|
||||
socketRef.value.on(SocketEventName.RoomCandidate, roomCandidate);
|
||||
};
|
||||
useSocket(handleRoomConnect);
|
||||
|
||||
return { ...roomInfo, rtcConnects, dataChanelMap, completed, startConnect };
|
||||
};
|
||||
|
||||
// 获取房间的一些信息,例如 peer 等信息
|
||||
export const useGetRoomInfo = () => {
|
||||
const members = ref<Partial<Member>[]>([]);
|
||||
const selfInfo = ref<OwnerInfo>({
|
||||
socketId: '',
|
||||
owner: false,
|
||||
recoderId: '',
|
||||
roomId: '',
|
||||
});
|
||||
|
||||
const roomCreated = (data: any) => {
|
||||
members.value = uniqBy(
|
||||
[
|
||||
...data.peers.map((peer: any) => ({
|
||||
id: peer.id,
|
||||
nickName: peer.nickName,
|
||||
owner: peer.owner,
|
||||
})),
|
||||
{
|
||||
id: data.id,
|
||||
owner: data.owner,
|
||||
nickName: data.nickName,
|
||||
},
|
||||
],
|
||||
'id'
|
||||
);
|
||||
|
||||
selfInfo.value = {
|
||||
socketId: data.id,
|
||||
owner: data.owner,
|
||||
recoderId: data.recoderId,
|
||||
roomId: data.room,
|
||||
};
|
||||
console.log('created', members, data);
|
||||
};
|
||||
|
||||
const roomExit = async (data: any) => {
|
||||
console.log('exit', data);
|
||||
members.value = members.value.filter((item) => item.id !== data.from);
|
||||
};
|
||||
|
||||
const roomJoin = async (data: any) => {
|
||||
console.log('join', data);
|
||||
members.value = uniqBy(
|
||||
[
|
||||
...members.value,
|
||||
{
|
||||
id: data.id,
|
||||
nickName: data.nickName,
|
||||
owner: data.owner,
|
||||
},
|
||||
],
|
||||
'id'
|
||||
);
|
||||
};
|
||||
|
||||
const roomOwner = computed(
|
||||
() => members.value.find((item) => item.owner) || undefined
|
||||
);
|
||||
|
||||
const self = computed(() => {
|
||||
const info =
|
||||
members.value.find((item) => item.id === selfInfo.value.socketId) ||
|
||||
undefined;
|
||||
|
||||
if (info) {
|
||||
return Object.assign({}, { ...info, roomInfo: selfInfo.value });
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const handleRoomInfo = async (socket: any) => {
|
||||
socket.on(SocketEventName.RoomCreated, roomCreated);
|
||||
socket.on(SocketEventName.RoomExit, roomExit);
|
||||
socket.on(SocketEventName.RoomJoin, roomJoin);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
console.log('执行');
|
||||
socket.emit(SocketEventName.RoomExit, {
|
||||
from: selfInfo.value.socketId,
|
||||
room: selfInfo.value.roomId,
|
||||
recoderId: selfInfo.value.recoderId,
|
||||
});
|
||||
socket.removeAllListeners();
|
||||
});
|
||||
};
|
||||
|
||||
useSocket(handleRoomInfo);
|
||||
|
||||
return {
|
||||
roomOwner,
|
||||
members,
|
||||
self,
|
||||
selfInfo,
|
||||
};
|
||||
};
|
||||
|
57
client/packages/rtc-web/src/hooks/useRouterReactive.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useRouteParams, useRouteQuery } from '@vueuse/router';
|
||||
import { Ref, toRefs, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
export const useRouteParamsReactive = <T extends string>(keys: T[]) => {
|
||||
const params = keys.reduce(
|
||||
(cur, next) => ({ ...cur, [next]: useRouteParams(next) }),
|
||||
{} as Record<T, Ref<string | string[] | null>>
|
||||
);
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
watch(
|
||||
() => route.params,
|
||||
(v) => {
|
||||
Object.keys(v).forEach((key) => {
|
||||
if (params[key as T]) {
|
||||
params[key as T].value = v[key];
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
export const useRouteQueryReactive = (
|
||||
...args: Parameters<typeof useRouteQuery>
|
||||
) => {
|
||||
const { query } = toRefs(useRoute());
|
||||
|
||||
const [key, defaultValue, options] = args;
|
||||
const valueRef = useRouteQuery(key, defaultValue, options);
|
||||
|
||||
watch(
|
||||
() => query.value[key],
|
||||
(v: any) => {
|
||||
const nv = options?.transform?.(v);
|
||||
if (options?.transform === Number) {
|
||||
if (!isNaN(nv as number)) {
|
||||
valueRef.value = nv;
|
||||
}
|
||||
} else {
|
||||
valueRef.value = nv;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
return valueRef;
|
||||
};
|
55
client/packages/rtc-web/src/hooks/useSwitchByUrl.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useWindowSize } from '@vueuse/core';
|
||||
import { Ref, computed, watch } from 'vue';
|
||||
import { useRouteQueryReactive } from './useRouterReactive';
|
||||
|
||||
export const useSwitchMember = () => {
|
||||
const { width } = useWindowSize();
|
||||
const showMembers = useRouteQueryReactive('showMembers', '0', {
|
||||
transform: Number,
|
||||
});
|
||||
|
||||
const switchMember = () => {
|
||||
showMembers.value = showMembers.value === 0 ? 1 : 0;
|
||||
};
|
||||
|
||||
const open = computed(() => showMembers.value === 1);
|
||||
|
||||
// 是否是宽屏, max-width = 1024
|
||||
const isLgScreen = computed(() => width.value <= 1024);
|
||||
|
||||
watch(
|
||||
() => isLgScreen.value,
|
||||
(w) => {
|
||||
showMembers.value = w ? 0 : 1;
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
open,
|
||||
switchMember,
|
||||
isLgScreen,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSwitchSiderbar = () => {
|
||||
const showSiderbar = useRouteQueryReactive('showSiderbar', '1', {
|
||||
transform: Number,
|
||||
}) as Ref<number>;
|
||||
|
||||
const switchSiderbar = () => {
|
||||
showSiderbar.value = showSiderbar.value === 0 ? 1 : 0;
|
||||
};
|
||||
|
||||
const open = computed(() => {
|
||||
return showSiderbar.value === 1;
|
||||
});
|
||||
|
||||
return {
|
||||
switchSiderbar,
|
||||
open,
|
||||
showSiderbar,
|
||||
};
|
||||
};
|
40
client/packages/rtc-web/src/hooks/useUseragent.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/* eslint-disable indent */
|
||||
export const useUserAgent = () => {
|
||||
const uerAgent = window.navigator.userAgent;
|
||||
|
||||
const isMobile = /Mobi|Android|iPhone/i.test(uerAgent);
|
||||
|
||||
const getNetWorkState = () => {
|
||||
let networkStr = uerAgent.match(/NetType\/\w+/)
|
||||
? uerAgent.match(/NetType\/\w+/)?.[0]
|
||||
: 'NetType/other';
|
||||
networkStr = networkStr?.toLowerCase().replace('nettype/', '');
|
||||
if (
|
||||
networkStr &&
|
||||
!['wifi', '5g', '3g', '4g', '2g', '3gnet', 'slow-2g'].includes(networkStr)
|
||||
) {
|
||||
if ((navigator as any).connection) {
|
||||
networkStr = (navigator as any).connection.effectiveType;
|
||||
}
|
||||
}
|
||||
switch (networkStr) {
|
||||
case 'wifi':
|
||||
return 'wifi';
|
||||
case '5g':
|
||||
return '5g';
|
||||
case '4g':
|
||||
return '4g';
|
||||
case '3g' || '3gnet':
|
||||
return '3g';
|
||||
case '2g' || 'slow-2g':
|
||||
return '2g';
|
||||
default:
|
||||
return 'unknow';
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isMobile,
|
||||
getNetWorkState,
|
||||
};
|
||||
};
|
15
client/packages/rtc-web/src/keys/server.crt
Normal file
@@ -0,0 +1,15 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICVTCCAb4CCQCCDZ6FebPIqjANBgkqhkiG9w0BAQsFADBvMQswCQYDVQQGEwJV
|
||||
UzENMAsGA1UECAwETWFyczETMBEGA1UEBwwKaVRyYW5zd2FycDETMBEGA1UECgwK
|
||||
aVRyYW5zd2FycDETMBEGA1UECwwKaVRyYW5zd2FycDESMBAGA1UEAwwJMTI3LjAu
|
||||
MC4xMB4XDTE4MDcxMDAzMTYzN1oXDTI4MDcwNzAzMTYzN1owbzELMAkGA1UEBhMC
|
||||
VVMxDTALBgNVBAgMBE1hcnMxEzARBgNVBAcMCmlUcmFuc3dhcnAxEzARBgNVBAoM
|
||||
CmlUcmFuc3dhcnAxEzARBgNVBAsMCmlUcmFuc3dhcnAxEjAQBgNVBAMMCTEyNy4w
|
||||
LjAuMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1I6AQ6eNez85kcKjwy3g
|
||||
/vcnXtw+EbP4Ab37fLhIIWG+XzmEBAqnCYjM3nmlDIGEfNylGReo9mD2OHg46a1D
|
||||
wjd3pxTMit41pCTCiu8S9A2UJfbhSzQrfs+IZcNye4KR9/FzNEW6KoKQ0uc6X33E
|
||||
0xe41hbRMQoKB3WmxvyN8PcCAwEAATANBgkqhkiG9w0BAQsFAAOBgQAd7GNDtKWA
|
||||
0OpSCzMu0pbmss9Erh4/RC8D+wK4+TXgPDKyZ6hX4FYPvk+ryMvwxJf0o4jjx5cx
|
||||
Yew7UjaKHlGXq+CNVRFYlltsbvO3oQTNkajuyYGWzMSuNxNsT3apOxH7SIu3qao8
|
||||
COSwj5FxZ2JU7O+SBVFZoJrFXEa+KJMQzQ==
|
||||
-----END CERTIFICATE-----
|
15
client/packages/rtc-web/src/keys/server.key
Normal file
@@ -0,0 +1,15 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXAIBAAKBgQDUjoBDp417PzmRwqPDLeD+9yde3D4Rs/gBvft8uEghYb5fOYQE
|
||||
CqcJiMzeeaUMgYR83KUZF6j2YPY4eDjprUPCN3enFMyK3jWkJMKK7xL0DZQl9uFL
|
||||
NCt+z4hlw3J7gpH38XM0RboqgpDS5zpffcTTF7jWFtExCgoHdabG/I3w9wIDAQAB
|
||||
AoGAWsy1BjGhQrDzisy24D3NC53Q97jl2vIiU7wwnkqqpXf3tv3+4ysZx/zkZ3VX
|
||||
iEwbqKso69srlnQ9OkpBJbGaa6lZe+z7BGzv2eJr+hKjjVjR122eDjAtXw+Tmt6c
|
||||
iBUG9+ITC1GdhXLEgTXtYuPq8hbDhoAVI007E+5JuQoO8kECQQD3aR6zGfR7EOmn
|
||||
byGkNMMPY/FoH894BpB4l7gIlNXH3pxBqukrEwnmVXSfak2PAkeLFO+bgCc6baIZ
|
||||
+R4iOLn/AkEA2++dIndbY7nmGs7Q//sM7MkFMiFQ4h1nN38V9AEHaUonxlwo+Nks
|
||||
PTAaVd78YIh6yBlfexm0Fxi1mEVQApqZCQJAFUPqyJglhGJqwuJxcMy8K1l6yWla
|
||||
isV9q2/W+J3aViiTI63OBs7HHg4gTQd1DSK0BYdSJPp55LLBqRvZdDWN/wJBAMQa
|
||||
M46exAL4p55xl9MWwyCB4LshD6B9vSGzlBx7qmMMNsjcNcAkzBhGwsScTYW5S1kN
|
||||
nqABfB037/s0mjGoLRkCQF18j4MovyFaj8VAqY8YUmf86Ez9JmL9kHNA8yEoSjuI
|
||||
xsRa6y5Nza5y83Mojt4W+PfS386riJ7txqrPdezyag4=
|
||||
-----END RSA PRIVATE KEY-----
|
@@ -1,20 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
import { MenuSide } from '@/components/menu';
|
||||
import { NavHeader, FullHeightFlexBox } from '@/components/lib';
|
||||
import { useInitData, useSwitchSiderbar } from '@/hooks';
|
||||
import { provide } from 'vue';
|
||||
import { InitDataKey } from '@/context';
|
||||
|
||||
defineOptions({
|
||||
name: 'LayoutIndex',
|
||||
});
|
||||
const { showSiderbar, open } = useSwitchSiderbar();
|
||||
|
||||
const { initData } = await useInitData();
|
||||
|
||||
provide(InitDataKey, initData);
|
||||
|
||||
const handleToggleSide = () => {
|
||||
showSiderbar.value = 1;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FullHeightFlexBox dire="col">
|
||||
<NavHeader />
|
||||
<NavHeader :showSiderbar="open" @toggle="handleToggleSide" />
|
||||
<FullHeightFlexBox type="full">
|
||||
<MenuSide
|
||||
v-if="open"
|
||||
class="hidden dark:shadow-sm dark:shadow-neutral-600 lg:block lg:shadow-md"
|
||||
/>
|
||||
<RouterView />
|
||||
<RouterView class="flex-1" />
|
||||
</FullHeightFlexBox>
|
||||
</FullHeightFlexBox>
|
||||
</template>
|
||||
|
@@ -8,7 +8,16 @@ import SvgIcon from '@/components/base/svg-icon.vue';
|
||||
|
||||
import App from './App.vue';
|
||||
|
||||
import VConsole from 'vconsole';
|
||||
import { useUserAgent } from '@/hooks';
|
||||
import { isDev } from '@/utils';
|
||||
|
||||
const { isMobile } = useUserAgent();
|
||||
|
||||
function setup() {
|
||||
if (isMobile && isDev()) {
|
||||
new VConsole();
|
||||
}
|
||||
return createApp(App).use(router).component('svg-icon', SvgIcon);
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,10 @@
|
||||
export const routes = [
|
||||
{ path: '/', component: () => import('@/views/welcome.vue') },
|
||||
{ path: '/chat/:roomId', component: () => import('@/views/chat/chat.vue') },
|
||||
{
|
||||
path: '/video/:roomId',
|
||||
component: () => import('@/views/video/video.vue'),
|
||||
},
|
||||
// 将匹配所有内容并将其放在 `$route.params.pathMatch` 下
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
|
@@ -10,3 +10,87 @@ export function withBtnClickEvent(fn: CommonFnType) {
|
||||
export function preventDefault(e: Event) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
export function safenExecuteConditioFn(condition: boolean, fn: CommonFnType) {
|
||||
if (condition) {
|
||||
fn();
|
||||
}
|
||||
}
|
||||
|
||||
// 将 socket on 转换为 promisify
|
||||
export function transformSocketListenEvent(socket: any, ev: string) {
|
||||
return new Promise<any[]>((resolve) => {
|
||||
const cb = (...args: any[]) => {
|
||||
resolve(args);
|
||||
};
|
||||
socket.on(ev, cb);
|
||||
});
|
||||
}
|
||||
|
||||
// 转义字符串
|
||||
export function escapeStr(str: string) {
|
||||
const entityMap: any = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/',
|
||||
'`': '`',
|
||||
'=': '=',
|
||||
};
|
||||
|
||||
const encodedMap: any = {
|
||||
'%': '%25',
|
||||
'!': '%21',
|
||||
"'": '%27',
|
||||
'(': '%28',
|
||||
')': '%29',
|
||||
'*': '%2A',
|
||||
'-': '%2D',
|
||||
'.': '%2E',
|
||||
_: '%5F',
|
||||
'~': '%7E',
|
||||
};
|
||||
|
||||
return String(str).replace(/[&<>"'`=/%!'()*\-._~]/g, function (s) {
|
||||
return entityMap[s] || encodedMap[s] || '';
|
||||
});
|
||||
}
|
||||
|
||||
export function unescapeStr(str: string) {
|
||||
const entityMap: any = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
''': "'",
|
||||
'/': '/',
|
||||
'`': '`',
|
||||
'=': '=',
|
||||
};
|
||||
const encodedMap: any = {
|
||||
'%25': '%',
|
||||
'%21': '!',
|
||||
'%27': "'",
|
||||
'%28': '(',
|
||||
'%29': ')',
|
||||
'%2A': '*',
|
||||
'%2D': '-',
|
||||
'%2E': '.',
|
||||
'%5F': '_',
|
||||
'%7E': '~',
|
||||
};
|
||||
return String(str).replace(
|
||||
/&(amp|lt|gt|quot|#39|#x2F|#x60|#x3D);|%(25|21|27|28|29|2A|2D|2E|5F|7E)/g,
|
||||
function (s) {
|
||||
return entityMap[s] || encodedMap[s] || '';
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export const isItBetween = (num: number, arr: number[]) => {
|
||||
return num >= arr[0] && num <= arr[1];
|
||||
};
|
||||
|
||||
export const resetUrl = () => window.location.replace('/');
|
||||
|
7
client/packages/rtc-web/src/utils/env.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const isProd = () => {
|
||||
return import.meta.env.PROD;
|
||||
};
|
||||
|
||||
export const isDev = () => {
|
||||
return import.meta.env.DEV;
|
||||
};
|
@@ -1 +1,4 @@
|
||||
export * from './common';
|
||||
export * from './user';
|
||||
export * from './reactive';
|
||||
export * from './env';
|
||||
|
503
client/packages/rtc-web/src/utils/user.ts
Normal file
@@ -0,0 +1,503 @@
|
||||
export const nameDataBase = () => {
|
||||
const adjectives = [
|
||||
'幽默的',
|
||||
'搞笑的',
|
||||
'疯狂的',
|
||||
'奇怪的',
|
||||
'古怪的',
|
||||
'无聊的',
|
||||
'神秘的',
|
||||
'魔幻的',
|
||||
'风趣的',
|
||||
'调皮的',
|
||||
'聪明的',
|
||||
'美丽的',
|
||||
'可爱的',
|
||||
'迷人的',
|
||||
'酷的',
|
||||
'萌萌的',
|
||||
'潇洒的',
|
||||
'霸气的',
|
||||
'猛烈的',
|
||||
'光芒的',
|
||||
'伶俐的',
|
||||
'俏皮的',
|
||||
'小巧的',
|
||||
'细腻的',
|
||||
'娇嫩的',
|
||||
'柔软的',
|
||||
'亲切的',
|
||||
'朴实的',
|
||||
'拘谨的',
|
||||
'高傲的',
|
||||
'自恋的',
|
||||
'浪漫的',
|
||||
'单纯的',
|
||||
'深情的',
|
||||
'执着的',
|
||||
'冷酷的',
|
||||
'刁蛮的',
|
||||
'天真的',
|
||||
'多情的',
|
||||
'成熟的',
|
||||
'忧郁的',
|
||||
'神经质的',
|
||||
'孤独的',
|
||||
'怀旧的',
|
||||
'清新的',
|
||||
'淡雅的',
|
||||
'冷艳的',
|
||||
'高冷的',
|
||||
'玩世不恭的',
|
||||
'逆天的',
|
||||
'暴躁的',
|
||||
'暴力的',
|
||||
'妩媚的',
|
||||
'狡猾的',
|
||||
'自信的',
|
||||
'自卑的',
|
||||
'悲观的',
|
||||
'乐观的',
|
||||
'勇敢的',
|
||||
'胆小的',
|
||||
'快乐的',
|
||||
'痛苦的',
|
||||
'善良的',
|
||||
'邪恶的',
|
||||
'深邃的',
|
||||
'神圣的',
|
||||
'丰满的',
|
||||
'单薄的',
|
||||
'肥胖的',
|
||||
'瘦弱的',
|
||||
'英俊的',
|
||||
'丑陋的',
|
||||
'芳香的',
|
||||
'臭气熏天的',
|
||||
'热情的',
|
||||
'冷漠的',
|
||||
'朝气蓬勃的',
|
||||
'干净的',
|
||||
'脏兮兮的',
|
||||
'无忧无虑的',
|
||||
'喜怒无常的',
|
||||
'平凡的',
|
||||
'非凡的',
|
||||
'害羞的',
|
||||
'热心的',
|
||||
'机智的',
|
||||
'敏捷的',
|
||||
'迟钝的',
|
||||
'聪慧的',
|
||||
'无知的',
|
||||
'真诚的',
|
||||
'虚伪的',
|
||||
'直率的',
|
||||
'谨慎的',
|
||||
'大胆的',
|
||||
'谦虚的',
|
||||
'傲慢的',
|
||||
'严肃的',
|
||||
'轻松的',
|
||||
'紧张的',
|
||||
'勤劳的',
|
||||
'懒惰的',
|
||||
'守时的',
|
||||
'迟到的',
|
||||
'坚强的',
|
||||
'软弱的',
|
||||
'聪慧的',
|
||||
'愚笨的',
|
||||
'机灵的',
|
||||
'迟钝的',
|
||||
'淘气的',
|
||||
'乖巧的',
|
||||
'活泼的',
|
||||
'沉默的',
|
||||
'健康的',
|
||||
'不健康的',
|
||||
'高大的',
|
||||
'矮小的',
|
||||
'长的',
|
||||
'短的',
|
||||
'胖的',
|
||||
'瘦的',
|
||||
'美满的',
|
||||
'不幸的',
|
||||
'富有的',
|
||||
'贫穷的',
|
||||
'快乐的',
|
||||
'不开心的',
|
||||
'甜美的',
|
||||
'苦涩的',
|
||||
'精明的',
|
||||
'愚蠢的',
|
||||
'聪明的',
|
||||
'智商高的',
|
||||
'心灵手巧的',
|
||||
'笨手笨脚的',
|
||||
'冷静的',
|
||||
'冲动的',
|
||||
'踏实的',
|
||||
'轻浮的',
|
||||
'温柔的',
|
||||
'粗暴的',
|
||||
'好学的',
|
||||
'讨厌学习的',
|
||||
'好吃的',
|
||||
'不好吃的',
|
||||
'耐心的',
|
||||
'急躁的',
|
||||
'友善的',
|
||||
'冷漠的',
|
||||
'豁达的',
|
||||
'固执的',
|
||||
'谨慎的',
|
||||
'善良的',
|
||||
'狠毒的',
|
||||
'平和的',
|
||||
'狂躁的',
|
||||
'机会主义的',
|
||||
'悲观的',
|
||||
'乐观的',
|
||||
'心胸开阔的',
|
||||
'偏狭的',
|
||||
'讲义气的',
|
||||
'不守信用的',
|
||||
'有魅力的',
|
||||
'无趣的',
|
||||
'有思想的',
|
||||
'无聊的',
|
||||
'谋略深的',
|
||||
'目光短浅的',
|
||||
'善解人意的',
|
||||
'自私的',
|
||||
'坦率的',
|
||||
'虚伪的',
|
||||
'好奇的',
|
||||
'不解风情的',
|
||||
'喜欢交友的',
|
||||
'独来独往的',
|
||||
'健谈的',
|
||||
'静默的',
|
||||
'喜欢思考的',
|
||||
'机智幽默的',
|
||||
'情感丰富的',
|
||||
'心地善良的',
|
||||
'充满自信的',
|
||||
'天真烂漫的',
|
||||
'追求完美的',
|
||||
'充满活力的',
|
||||
'喜欢冒险的',
|
||||
'充满创造力的',
|
||||
'沉着冷静的',
|
||||
'目标明确的',
|
||||
'性格温和的',
|
||||
'乐于助人的',
|
||||
'聪明伶俐的',
|
||||
'重情重义的',
|
||||
'思维敏捷的',
|
||||
'慷慨大方的',
|
||||
'婉约多姿的',
|
||||
'时尚前卫的',
|
||||
'豁达开朗的',
|
||||
'气质高雅的',
|
||||
'优雅大方的',
|
||||
'沉静深沉的',
|
||||
'坚韧不拔的',
|
||||
'独立自主的',
|
||||
'外向开朗的',
|
||||
'内向沉默的',
|
||||
'深情专注的',
|
||||
'精力旺盛的',
|
||||
'富于幽默的',
|
||||
'心思细腻的',
|
||||
'喜怒形于色的',
|
||||
'忠心耿耿的',
|
||||
'玩世不恭的',
|
||||
'活力四射的',
|
||||
'脚踏实地的',
|
||||
'注重细节的',
|
||||
'保守谨慎的',
|
||||
'世故圆滑的',
|
||||
'梦想家的',
|
||||
'勇往直前的',
|
||||
'干练果敢的',
|
||||
'待人友善的',
|
||||
'思想开放的',
|
||||
'敢于挑战的',
|
||||
'感性洒脱的',
|
||||
'洒脱不羁的',
|
||||
'自我牺牲的',
|
||||
'处事果断的',
|
||||
'好奇心强的',
|
||||
'待人热情的',
|
||||
'热情洋溢的',
|
||||
'孤独悲伤的',
|
||||
'浪漫多情的',
|
||||
'爱笑的',
|
||||
'不羁的',
|
||||
'傻气的',
|
||||
'不拘小节的',
|
||||
'懒散的',
|
||||
'无聊的',
|
||||
'低调的',
|
||||
'敏感的',
|
||||
'冷酷的',
|
||||
'专注的',
|
||||
'不屑的',
|
||||
'激情的',
|
||||
'忠诚的',
|
||||
'神秘的',
|
||||
'高傲的',
|
||||
'自由的',
|
||||
'文艺的',
|
||||
'时尚的',
|
||||
'落落大方的',
|
||||
'有才华的',
|
||||
'有气质的',
|
||||
'阳光的',
|
||||
'风趣的',
|
||||
'天真浪漫的',
|
||||
'爽朗开朗的',
|
||||
'内敛沉静的',
|
||||
'刻苦努力的',
|
||||
'性格迥异的',
|
||||
'个性张扬的',
|
||||
'脾气火爆的',
|
||||
'傲娇的',
|
||||
'爱撒娇的',
|
||||
'心思缜密的',
|
||||
'理智果断的',
|
||||
'懒惰的',
|
||||
'喜欢拖延的',
|
||||
'有责任感的',
|
||||
'追求自由的',
|
||||
'感性的',
|
||||
'理性的',
|
||||
'缺乏安全感的',
|
||||
'追求安全感的',
|
||||
'情绪化的',
|
||||
'乐观的',
|
||||
'悲观的',
|
||||
'现实主义的',
|
||||
'理想主义的',
|
||||
'平易近人的',
|
||||
'目中无人的',
|
||||
'重视亲情的',
|
||||
'重视友情的',
|
||||
'重视爱情的',
|
||||
'有爱心的',
|
||||
'有正义感的',
|
||||
'有同情心的',
|
||||
'有童心的',
|
||||
'有自信的',
|
||||
'胆小怕事的',
|
||||
'爱唠叨的',
|
||||
'话多的',
|
||||
'话少的',
|
||||
'勇于冒险的',
|
||||
'爱挑战的',
|
||||
'善于发现美的',
|
||||
'自我意识强的',
|
||||
'不喜欢被约束的',
|
||||
'慢热型的',
|
||||
'热情洋溢的',
|
||||
'容易受伤的',
|
||||
'重视感情的',
|
||||
'善于沟通的',
|
||||
'不善于表达的',
|
||||
'有幽默感的',
|
||||
'平易近人的',
|
||||
'有亲和力的',
|
||||
'脸皮厚的',
|
||||
'喜欢交际的',
|
||||
'宅男/宅女的',
|
||||
'喜欢独处的',
|
||||
'有自知之明的',
|
||||
'喜欢音乐的',
|
||||
'喜欢阅读的',
|
||||
'喜欢旅行的',
|
||||
'喜欢美食的',
|
||||
'喜欢运动的',
|
||||
'喜欢摄影的',
|
||||
'喜欢收藏的',
|
||||
'喜欢购物的',
|
||||
'不喜欢出门的',
|
||||
'喜欢挑剔的',
|
||||
'喜欢自省的',
|
||||
'不喜欢评价他人的',
|
||||
'喜欢评价他人的',
|
||||
'有品位的',
|
||||
'不讲卫生的',
|
||||
'讲究卫生的',
|
||||
'喜欢干净的',
|
||||
'喜欢乱的',
|
||||
'喜欢组织的',
|
||||
'喜欢随意的',
|
||||
'喜欢收纳的',
|
||||
];
|
||||
const nouns = [
|
||||
'狗子',
|
||||
'猫咪',
|
||||
'小鹿',
|
||||
'小熊',
|
||||
'小兔',
|
||||
'小羊',
|
||||
'小猪',
|
||||
'小马',
|
||||
'小狮子',
|
||||
'小老虎',
|
||||
'小猴子',
|
||||
'小鱼儿',
|
||||
'小乌龟',
|
||||
'小鸟儿',
|
||||
'小蚂蚁',
|
||||
'小蜜蜂',
|
||||
'小蝴蝶',
|
||||
'小蜻蜓',
|
||||
'小螃蟹',
|
||||
'小章鱼',
|
||||
'小海豚',
|
||||
'小鲨鱼',
|
||||
'小鲸鱼',
|
||||
'小鳄鱼',
|
||||
'小鸭子',
|
||||
'小雪人',
|
||||
'小皮球',
|
||||
'小篮球',
|
||||
'小足球',
|
||||
'小排球',
|
||||
'小棒球',
|
||||
'小滑板',
|
||||
'小冰棍',
|
||||
'小雨伞',
|
||||
'小手套',
|
||||
'小电影',
|
||||
'小蓝天',
|
||||
'小公主',
|
||||
'小王子',
|
||||
'小玩具',
|
||||
'小糖果',
|
||||
'小巧克力',
|
||||
'小冰淇淋',
|
||||
'小蛋糕',
|
||||
'小披萨',
|
||||
'小汉堡',
|
||||
'小炸鸡',
|
||||
'小烤鸭',
|
||||
'小鱼丸',
|
||||
'小火锅',
|
||||
'小串串',
|
||||
'小煎饼',
|
||||
'小油条',
|
||||
'小葱油饼',
|
||||
'小米粥',
|
||||
'小酸奶',
|
||||
'小豆腐',
|
||||
'小饺子',
|
||||
'小包子',
|
||||
'小馄饨',
|
||||
'小面条',
|
||||
'小牛肉面',
|
||||
'小糯米鸡',
|
||||
'小蒸饺',
|
||||
'小炒面',
|
||||
'小蒸包',
|
||||
'小烤肉',
|
||||
'小烤串',
|
||||
'小花生米',
|
||||
'小太阳',
|
||||
'小月亮',
|
||||
'小星星',
|
||||
'小彩虹',
|
||||
'小风车',
|
||||
'小气球',
|
||||
'小钢琴',
|
||||
'小吉他',
|
||||
'小音响',
|
||||
'小麦克风',
|
||||
'小演员',
|
||||
'小画家',
|
||||
'小工程师',
|
||||
'小医生',
|
||||
'小警察',
|
||||
'小消防员',
|
||||
'小司机',
|
||||
'小农民',
|
||||
'小潜水员',
|
||||
'小飞行员',
|
||||
'小篮球',
|
||||
'小游泳健将',
|
||||
'小跑步冠军',
|
||||
'小武术高手',
|
||||
'小芭蕾舞者',
|
||||
'小沙画家',
|
||||
'小书法家',
|
||||
'小拼图专家',
|
||||
'小玩具收藏家',
|
||||
'小电影制片人',
|
||||
'小太空旅行家',
|
||||
'超级英雄',
|
||||
'无敌大魔王',
|
||||
'终极霸主',
|
||||
'至尊帝王',
|
||||
'天降巨人',
|
||||
'绝世奇才',
|
||||
'神话之门',
|
||||
'恐怖怪兽',
|
||||
'魔法使者',
|
||||
'神秘剑客',
|
||||
'不朽传说',
|
||||
'宇宙霸主',
|
||||
'地狱火山',
|
||||
'无尽黑暗',
|
||||
'闪耀之星',
|
||||
'璀璨之光',
|
||||
'金色骑士',
|
||||
'毁天灭地',
|
||||
'战无不胜',
|
||||
'碾压一切',
|
||||
'绝世高手',
|
||||
'超凡脱俗',
|
||||
'万象之王',
|
||||
'黑暗骑士',
|
||||
'霸天战神',
|
||||
'万众瞩目',
|
||||
'震古烁今',
|
||||
'纵横天下',
|
||||
'永不磨灭',
|
||||
'恒久不变',
|
||||
'帝国之主',
|
||||
'不屈不挠',
|
||||
'狂暴之王',
|
||||
'超越极限',
|
||||
'魔力无边',
|
||||
'星光闪耀',
|
||||
'无尽追求',
|
||||
'刀锋之舞',
|
||||
'独步天下',
|
||||
'吞噬万物',
|
||||
'永恒之境',
|
||||
'灭世战神',
|
||||
'海量财富',
|
||||
'神话传说',
|
||||
'唯我独尊',
|
||||
'万剑归宗',
|
||||
'嗜血狂魔',
|
||||
'深海之王',
|
||||
'幻想之城',
|
||||
'天命之子',
|
||||
];
|
||||
return { adjectives, nouns };
|
||||
};
|
||||
|
||||
export function genNickName() {
|
||||
const { adjectives, nouns } = nameDataBase();
|
||||
const adjectiveIndex = Math.floor(Math.random() * adjectives.length);
|
||||
const nounIndex = Math.floor(Math.random() * nouns.length);
|
||||
const adjective = adjectives[adjectiveIndex];
|
||||
const noun = nouns[nounIndex];
|
||||
const randomNum = Math.floor(Math.random() * 1000);
|
||||
return adjective + noun + randomNum;
|
||||
}
|
119
client/packages/rtc-web/src/views/chat/chat-room.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<script lang="ts" setup>
|
||||
import { ChatContent, ChatRoomUser, ChatInput } from '@/components/chat-room';
|
||||
import MenuAction from '@/components/menu-action.vue';
|
||||
import { ChatInputAction } from '@/config';
|
||||
import { useSwitchMember } from '@/hooks';
|
||||
import { useGetRoomInfo } from '@/hooks/useRoom';
|
||||
import { useChat } from './hooks/useChat';
|
||||
import { escapeStr } from '@/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import { computed, ref, nextTick } from 'vue';
|
||||
import { useDragChangeSize } from '@/hooks';
|
||||
|
||||
defineOptions({
|
||||
name: 'ChatRoomCom',
|
||||
});
|
||||
|
||||
const { open } = useSwitchMember();
|
||||
|
||||
const { members, roomOwner, self } = useGetRoomInfo();
|
||||
|
||||
const { sendMessage, msgList } = useChat();
|
||||
|
||||
const inputMsg = ref('');
|
||||
|
||||
const chatMsgBoxRef = ref<any>(null);
|
||||
const chatMsgContentRef = ref<Element | null>(null);
|
||||
|
||||
const { canDragged, style } = useDragChangeSize(chatMsgBoxRef, {
|
||||
initialSize: { height: '260px' },
|
||||
position: 'top',
|
||||
persistentSize: {
|
||||
width: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
const reSizeClass = computed(() => {
|
||||
if (canDragged.value.draggable) {
|
||||
if (canDragged.value.position === 'top') {
|
||||
return ['cursor-ns-resize'];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const handleSendmsg = (msg: string) => {
|
||||
if (self.value) {
|
||||
const { roomInfo, nickName = '' } = self.value;
|
||||
sendMessage({
|
||||
content: escapeStr(msg),
|
||||
room: roomInfo?.roomId || '',
|
||||
from: roomInfo?.socketId || '',
|
||||
nickName,
|
||||
recoderId: roomInfo?.recoderId,
|
||||
time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
});
|
||||
|
||||
nextTick(() => {
|
||||
chatMsgContentRef?.value?.scroll({
|
||||
top: chatMsgContentRef.value.scrollHeight,
|
||||
left: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const msgContent = computed(() =>
|
||||
msgList.value.map((item) => ({
|
||||
message: item.content,
|
||||
time: item.time,
|
||||
username: item.nickName,
|
||||
reverse: item.type === 'send',
|
||||
}))
|
||||
);
|
||||
|
||||
const handleEmojiChange = (data: any) => {
|
||||
const unicode = parseInt(`0x${data.r}`, 16);
|
||||
inputMsg.value += String.fromCodePoint(unicode);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full">
|
||||
<div class="flex flex-1 flex-col">
|
||||
<!-- 这里得多加一个 div 才能 scroll -->
|
||||
<div
|
||||
ref="chatMsgContentRef"
|
||||
class="ces min-h-0 flex-1 overflow-y-auto overflow-x-hidden px-4 py-4"
|
||||
>
|
||||
<ChatContent :msg-list="msgContent" />
|
||||
</div>
|
||||
<div
|
||||
ref="chatMsgBoxRef"
|
||||
:style="style"
|
||||
:class="[...reSizeClass]"
|
||||
class="flex h-[260px] max-h-[610px] flex-col border-t pb-1.5 dark:border-neutral-600"
|
||||
>
|
||||
<MenuAction
|
||||
:menu-action="ChatInputAction"
|
||||
class="pl-2 pt-2"
|
||||
@emoji-change="handleEmojiChange"
|
||||
/>
|
||||
<ChatInput
|
||||
v-model:msg="inputMsg"
|
||||
class="mt-2 flex-1"
|
||||
@send-msg="handleSendmsg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ChatRoomUser
|
||||
v-show="open"
|
||||
class="hidden max-w-[400px] border-l dark:border-neutral-600 lg:block"
|
||||
width="220px"
|
||||
:members="members"
|
||||
:room-owner="roomOwner"
|
||||
:self="self"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
@@ -1,27 +1,67 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useRoom } from '@/hooks';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useCreateRoom } from '@/hooks';
|
||||
import { BackPreviousLevel, BackTitle } from '@/components/back';
|
||||
import ChatRoomCom from './chat-room.vue';
|
||||
import MenuAction from '@/components/menu-action.vue';
|
||||
import { ChatAction } from '@/config';
|
||||
import { useSwitchMember } from '@/hooks';
|
||||
|
||||
defineOptions({
|
||||
name: 'ChatView',
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const { params } = useRoute();
|
||||
|
||||
const { isValid } = useRoom((params.roomId as string) || '');
|
||||
const { roomId } = useCreateRoom();
|
||||
|
||||
const checkParams = () => {
|
||||
console.log(isValid.value);
|
||||
if (!isValid.value) router.replace('/');
|
||||
const { switchMember, open, isLgScreen } = useSwitchMember();
|
||||
|
||||
const chatActionMenu = ref(ChatAction);
|
||||
|
||||
watch(
|
||||
() => open.value,
|
||||
(v) => {
|
||||
const findv = chatActionMenu.value.find((item) => item.name === 'member');
|
||||
if (findv) {
|
||||
findv.color = v ? '#2F54EB' : undefined;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const handleBackLevel = () => router.replace('/');
|
||||
|
||||
const handleClickIcon = (name: string) => {
|
||||
if (name === 'member' && !isLgScreen.value) {
|
||||
switchMember();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(checkParams);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>我是chat - {{ params.roomId }}</div>
|
||||
<div class="flex w-full flex-col">
|
||||
<BackPreviousLevel
|
||||
class="py-2 shadow-md dark:shadow-sm dark:shadow-neutral-600"
|
||||
@back="handleBackLevel"
|
||||
>
|
||||
<BackTitle svg-name="chat" class="flex-1 justify-between">
|
||||
<div class="cursor-pointer">
|
||||
{{ roomId }}
|
||||
</div>
|
||||
<MenuAction
|
||||
:menu-action="chatActionMenu"
|
||||
class="mr-6"
|
||||
@click-icon="handleClickIcon"
|
||||
/>
|
||||
</BackTitle>
|
||||
</BackPreviousLevel>
|
||||
|
||||
<div class="min-h-0 flex-1">
|
||||
<ChatRoomCom />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
57
client/packages/rtc-web/src/views/chat/hooks/useChat.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { ChatEventName } from '@/config';
|
||||
import { useSocket } from '@/hooks';
|
||||
import { unescapeStr } from '@/utils';
|
||||
import { ref, shallowRef } from 'vue';
|
||||
|
||||
export type SendMessage = {
|
||||
content: string;
|
||||
room: string;
|
||||
from: string;
|
||||
nickName: string;
|
||||
recoderId: any;
|
||||
time: string;
|
||||
to?: string;
|
||||
};
|
||||
|
||||
// export type MessageStatus = 'fail' | 'complete' | 'pending';
|
||||
export type MessageType = 'send' | 'receive';
|
||||
|
||||
export type MessageInfo = {
|
||||
content: string;
|
||||
// status: MessageStatus;
|
||||
type: MessageType;
|
||||
} & SendMessage;
|
||||
|
||||
export const useChat = () => {
|
||||
const socketRef = shallowRef();
|
||||
const msgList = ref<MessageInfo[]>([]);
|
||||
|
||||
const handleChat = (socket: any) => {
|
||||
socketRef.value = socket;
|
||||
socket.on(ChatEventName.ChatingRoom, handleChatingRoom);
|
||||
};
|
||||
const handleChatingRoom = (data: SendMessage) => {
|
||||
console.log('receive', data);
|
||||
msgList.value.push({
|
||||
...data,
|
||||
content: unescapeStr(data.content),
|
||||
type: 'receive',
|
||||
});
|
||||
};
|
||||
|
||||
const sendMessage = (data: SendMessage) => {
|
||||
socketRef.value.emit(ChatEventName.ChatingRoom, data);
|
||||
msgList.value.push({
|
||||
...data,
|
||||
content: unescapeStr(data.content),
|
||||
type: 'send',
|
||||
});
|
||||
};
|
||||
|
||||
useSocket(handleChat);
|
||||
|
||||
return {
|
||||
sendMessage,
|
||||
msgList,
|
||||
};
|
||||
};
|
243
client/packages/rtc-web/src/views/video/hooks/useVideoCall.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
import { useRoomConnect, useUserAgent } from '@/hooks';
|
||||
import { useUserMedia, useDevicesList } from '@vueuse/core';
|
||||
import { reject } from 'lodash';
|
||||
import {
|
||||
Ref,
|
||||
computed,
|
||||
onBeforeUnmount,
|
||||
ref,
|
||||
shallowRef,
|
||||
watch,
|
||||
watchEffect,
|
||||
} from 'vue';
|
||||
|
||||
export type VideoShareOption = {
|
||||
immeately?: boolean;
|
||||
audio?: Ref<MediaDeviceInfo | undefined>;
|
||||
speaker?: Ref<string>;
|
||||
};
|
||||
|
||||
export const useMediaSetting = (
|
||||
option: VideoShareOption = { immeately: true }
|
||||
) => {
|
||||
const currentCamera = ref<string>();
|
||||
const currentAudioInput = ref<string>();
|
||||
const currentAudioOutput = ref<string>();
|
||||
|
||||
const audioRef = ref(option.audio);
|
||||
const speakerRef = ref(option.speaker);
|
||||
|
||||
const mediaLoaded = ref(false);
|
||||
|
||||
const { isMobile } = useUserAgent();
|
||||
|
||||
// 切换 麦克风
|
||||
watch(
|
||||
audioRef,
|
||||
(value) => {
|
||||
currentAudioInput.value = value?.deviceId;
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
// 切换 扬声器
|
||||
watch(
|
||||
speakerRef,
|
||||
(value) => {
|
||||
currentAudioOutput.value = value;
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
// 收集的错误信息
|
||||
// const errorMsg = ref({
|
||||
// audio: '',
|
||||
// });
|
||||
|
||||
const {
|
||||
videoInputs: cameras,
|
||||
audioInputs,
|
||||
audioOutputs,
|
||||
} = useDevicesList({
|
||||
requestPermissions: true,
|
||||
onUpdated() {
|
||||
// 初始化默认 摄像头、扬声器、麦克风
|
||||
|
||||
if (!cameras.value.find((i) => i.deviceId === currentCamera.value))
|
||||
currentCamera.value = cameras.value[0]?.deviceId;
|
||||
|
||||
if (
|
||||
!audioInputs.value.find((i) => i.deviceId === currentAudioInput.value)
|
||||
)
|
||||
currentAudioInput.value = audioInputs.value[0]?.deviceId;
|
||||
|
||||
if (
|
||||
!audioOutputs.value.find((i) => i.deviceId === currentAudioOutput.value)
|
||||
) {
|
||||
currentAudioOutput.value = audioOutputs.value[0]?.deviceId;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const video = shallowRef<HTMLVideoElement>();
|
||||
|
||||
watch([currentAudioOutput, video], ([v1, v2]) => {
|
||||
if (v1 && v2) {
|
||||
setSinkId(v1);
|
||||
}
|
||||
});
|
||||
|
||||
const constraints = computed(() => {
|
||||
const audioDevice = currentAudioInput.value
|
||||
? { deviceId: currentAudioInput.value }
|
||||
: false;
|
||||
|
||||
const videoDevice = currentCamera.value
|
||||
? {
|
||||
deviceId: currentCamera.value,
|
||||
facingMode: 'user',
|
||||
frameRate: {
|
||||
ideal: 30,
|
||||
max: 60,
|
||||
},
|
||||
width: { ideal: 1920 },
|
||||
height: { ideal: 1280 },
|
||||
}
|
||||
: false;
|
||||
return {
|
||||
audio: audioDevice,
|
||||
video: videoDevice,
|
||||
};
|
||||
});
|
||||
|
||||
const { stream, stop, restart, start, isSupported } = useUserMedia({
|
||||
constraints,
|
||||
});
|
||||
|
||||
const audioTracks = ref<MediaStreamTrack[]>([]);
|
||||
const videoTracks = ref<MediaStreamTrack[]>([]);
|
||||
|
||||
const audioEnabled = ref(false);
|
||||
const videoEnabled = ref(false);
|
||||
|
||||
// 切换 video、audio 渲染
|
||||
const switchTrackEnable = (type: 'video' | 'audio', flag: boolean) => {
|
||||
if (stream.value) {
|
||||
if (type === 'audio') {
|
||||
if (audioTracks.value?.length) {
|
||||
audioEnabled.value = flag;
|
||||
audioTracks.value.forEach((item) => (item.enabled = flag));
|
||||
}
|
||||
}
|
||||
if (type === 'video') {
|
||||
if (videoTracks.value?.length) {
|
||||
videoEnabled.value = flag;
|
||||
videoTracks.value.forEach((item) => {
|
||||
item.enabled = flag;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const setSinkId = (sinkId: string) => {
|
||||
if (!isMobile) {
|
||||
(video.value! as any).setSinkId(sinkId);
|
||||
}
|
||||
};
|
||||
|
||||
// 先静音和关闭视频渲染
|
||||
watch([stream, video], () => {
|
||||
if (stream.value) {
|
||||
if (video.value) {
|
||||
video.value.srcObject = stream.value!;
|
||||
if (stream.value) {
|
||||
audioTracks.value = stream.value.getAudioTracks();
|
||||
videoTracks.value = stream.value.getVideoTracks();
|
||||
}
|
||||
if (!videoEnabled.value) {
|
||||
switchTrackEnable('video', false);
|
||||
}
|
||||
if (!audioEnabled.value) {
|
||||
switchTrackEnable('audio', false);
|
||||
}
|
||||
mediaLoaded.value = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 进入页面先连接
|
||||
const startGetMedia = () => {
|
||||
console.log(isSupported.value);
|
||||
return new Promise<MediaStream | undefined>((resolve) => {
|
||||
const watchEnableStop = watchEffect(async () => {
|
||||
if (
|
||||
currentAudioInput.value &&
|
||||
currentCamera.value &&
|
||||
currentAudioOutput.value
|
||||
) {
|
||||
const stream = await start();
|
||||
watchEnableStop();
|
||||
resolve(stream);
|
||||
} else {
|
||||
reject('请打开摄像头或者麦克风');
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (option.immeately) {
|
||||
startGetMedia();
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stop();
|
||||
});
|
||||
|
||||
return {
|
||||
startGetMedia,
|
||||
stream,
|
||||
video,
|
||||
setSinkId,
|
||||
switchTrackEnable,
|
||||
restart,
|
||||
stop,
|
||||
audioEnabled,
|
||||
videoEnabled,
|
||||
mediaLoaded,
|
||||
currentAudioInput,
|
||||
currentAudioOutput,
|
||||
};
|
||||
};
|
||||
|
||||
export const useMediaConnect = (
|
||||
stream: MediaStream,
|
||||
connect: { onTrack?: (track: MediaStreamTrack, id: string) => void } = {}
|
||||
) => {
|
||||
useRoomConnect({
|
||||
roomJoined: async (_, pc) => {
|
||||
if (pc && stream) {
|
||||
stream.getTracks().forEach((track) => {
|
||||
pc.addTrack(track, stream);
|
||||
});
|
||||
}
|
||||
},
|
||||
onBeforeCreateAnswer: async (_, pc) => {
|
||||
if (pc && stream) {
|
||||
stream.getTracks().forEach((track) => {
|
||||
pc.addTrack(track, stream);
|
||||
});
|
||||
}
|
||||
},
|
||||
onTrack(e: any, id: string) {
|
||||
// console.log(e);
|
||||
if (e.track.kind === 'video') {
|
||||
connect.onTrack?.(e.streams[0], id);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
158
client/packages/rtc-web/src/views/video/video-control-menu.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<script lang="ts" setup>
|
||||
import { VideoControlMenuAction } from '@/config';
|
||||
import { Modal } from '@/components/base';
|
||||
|
||||
import MenuAction from '@/components/menu-action.vue';
|
||||
import { ref } from 'vue';
|
||||
import { useDevicesList } from '@vueuse/core';
|
||||
import SelectList from '@/components/list/select-list.vue';
|
||||
import { Dropdown } from '@/components/base';
|
||||
import { PropType } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { resetUrl } from '@/utils';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'VideoControlMenu',
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
activeControlMenu: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => [],
|
||||
},
|
||||
menuDisabled: {
|
||||
type: Boolean as PropType<boolean>,
|
||||
default: false,
|
||||
},
|
||||
selectDevice: {
|
||||
type: Object as PropType<Record<'audioInput' | 'audioOutput', string>>,
|
||||
default: () => ({ audioInput: '', audioOutput: '' }),
|
||||
},
|
||||
});
|
||||
|
||||
const modalVisible = ref(false);
|
||||
|
||||
const emits = defineEmits(['controlMenuChange', 'selectDevice']);
|
||||
|
||||
const menuAction = computed(() => {
|
||||
return VideoControlMenuAction.map((item) => ({
|
||||
...item,
|
||||
disabled: props.menuDisabled,
|
||||
color: props.activeControlMenu.includes(item.name) ? '#2F54EB' : undefined,
|
||||
}));
|
||||
});
|
||||
|
||||
const { audioInputs, audioOutputs } = useDevicesList();
|
||||
|
||||
const audioDropDownVisible = ref(false);
|
||||
|
||||
const selectedAudioDevice = (device: MediaDeviceInfo) => {
|
||||
emits('selectDevice', device);
|
||||
audioDropDownVisible.value = false;
|
||||
};
|
||||
|
||||
const handleClickIcon = (name: string) => {
|
||||
if (name === 'hang-up') {
|
||||
modalVisible.value = true;
|
||||
}
|
||||
emits('controlMenuChange', name);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
modalVisible.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="video-control-menu ml-[50%] flex translate-x-[-50%]">
|
||||
<div class="control-camera flex">
|
||||
<button
|
||||
class="btn-circle btn"
|
||||
:disabled="menuDisabled"
|
||||
@click="handleClickIcon('audio')"
|
||||
>
|
||||
<svg-icon
|
||||
name="audio"
|
||||
:color="
|
||||
props.activeControlMenu.includes('audio') ? '#2F54EB' : '#707070'
|
||||
"
|
||||
class="h-6 w-6"
|
||||
/>
|
||||
</button>
|
||||
<Dropdown v-model:visible="audioDropDownVisible">
|
||||
<svg-icon
|
||||
name="up"
|
||||
class="h-4 w-4 cursor-pointer"
|
||||
color="#707070"
|
||||
@click="audioDropDownVisible = true"
|
||||
/>
|
||||
<template #content>
|
||||
<div class="panel">
|
||||
<SelectList
|
||||
title="选择麦克风"
|
||||
:list="audioInputs"
|
||||
@selected="selectedAudioDevice"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div
|
||||
:class="[
|
||||
props.selectDevice.audioInput === item.deviceId
|
||||
? 'text-[#2F54EB]'
|
||||
: '',
|
||||
]"
|
||||
>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</template>
|
||||
</SelectList>
|
||||
<SelectList
|
||||
title="选择扬声器"
|
||||
:list="audioOutputs"
|
||||
@selected="selectedAudioDevice"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div
|
||||
:class="[
|
||||
props.selectDevice.audioOutput === item.deviceId
|
||||
? 'text-[#2F54EB]'
|
||||
: '',
|
||||
]"
|
||||
>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</template>
|
||||
</SelectList>
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<MenuAction
|
||||
:menu-action="menuAction"
|
||||
:gap="8"
|
||||
@click-icon="handleClickIcon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Modal v-model:visible="modalVisible" :modal="false">
|
||||
<template #content> 确定要结束通话吗? </template>
|
||||
<template #action>
|
||||
<div class="flex justify-end">
|
||||
<button class="btn-neutral btn mr-4" @click.prevent="closeModal">
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
class="btn-info btn"
|
||||
@click.prevent="
|
||||
() => {
|
||||
nextTick(resetUrl);
|
||||
}
|
||||
"
|
||||
>
|
||||
确定
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
42
client/packages/rtc-web/src/views/video/video-control.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
import MenuAction from '@/components/menu-action.vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { VideoMenuAction } from '@/config';
|
||||
import { useSwitchMember } from '@/hooks';
|
||||
import VideoControlMenu from './video-control-menu.vue';
|
||||
import { useAttrs } from 'vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'VideoControl',
|
||||
});
|
||||
|
||||
const attrs = useAttrs();
|
||||
|
||||
const chatActionMenu = ref(VideoMenuAction);
|
||||
|
||||
const { switchMember, isLgScreen, open } = useSwitchMember();
|
||||
|
||||
watch(
|
||||
() => open.value,
|
||||
(v) => {
|
||||
const findv = chatActionMenu.value.find((item) => item.name === 'member');
|
||||
if (findv) {
|
||||
findv.color = v ? '#2F54EB' : undefined;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const handleClickIcon = (name: string) => {
|
||||
if (name === 'member' && !isLgScreen.value) {
|
||||
switchMember();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between px-4 py-4">
|
||||
<VideoControlMenu v-bind="attrs" />
|
||||
<MenuAction :menu-action="chatActionMenu" @click-icon="handleClickIcon" />
|
||||
</div>
|
||||
</template>
|
162
client/packages/rtc-web/src/views/video/video-room.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<script lang="ts" setup>
|
||||
import { useSwitchSiderbar, useSwitchMember, useGetRoomInfo } from '@/hooks';
|
||||
import { ref } from 'vue';
|
||||
import { useMediaConnect, useMediaSetting } from './hooks/useVideoCall';
|
||||
import VideoControl from './video-control.vue';
|
||||
import { computed } from 'vue';
|
||||
import { useCreateRoom } from '@/hooks';
|
||||
import { ToastBox } from '@/components/base';
|
||||
import { watch } from 'vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'VideoRoom',
|
||||
});
|
||||
|
||||
const { showSiderbar } = useSwitchSiderbar();
|
||||
showSiderbar.value = 0;
|
||||
|
||||
const { open } = useSwitchMember();
|
||||
|
||||
const audioInputDevice = ref<MediaDeviceInfo | undefined>();
|
||||
const speakerRef = ref('');
|
||||
|
||||
const otherVideo = ref<{ id: string; ref: any }[]>([]);
|
||||
|
||||
const collectOtherVideoRef = (ref: any, id: string) => {
|
||||
if (!otherVideo.value.find((other) => other.id === id)) {
|
||||
otherVideo.value.push({ id, ref });
|
||||
}
|
||||
};
|
||||
|
||||
const toastInfo = ref({
|
||||
visible: false,
|
||||
msg: '',
|
||||
});
|
||||
|
||||
const {
|
||||
startGetMedia,
|
||||
video,
|
||||
switchTrackEnable,
|
||||
videoEnabled,
|
||||
audioEnabled,
|
||||
mediaLoaded,
|
||||
currentAudioInput,
|
||||
currentAudioOutput,
|
||||
} = useMediaSetting({
|
||||
immeately: false,
|
||||
audio: audioInputDevice,
|
||||
speaker: speakerRef,
|
||||
});
|
||||
|
||||
// 获取摄像头、麦克风权限
|
||||
const stream = await startGetMedia().catch(() => {
|
||||
console.log('没有stream');
|
||||
});
|
||||
|
||||
// 创建房间
|
||||
useCreateRoom('video', true);
|
||||
|
||||
// 视频通话
|
||||
useMediaConnect(stream!, {
|
||||
onTrack(track, id) {
|
||||
otherVideo.value.forEach((item) => {
|
||||
if (item.id === id) {
|
||||
item.ref.srcObject = track;
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { members, selfInfo } = useGetRoomInfo();
|
||||
|
||||
watch(selfInfo, (v) => {
|
||||
if (v) {
|
||||
toastInfo.value.visible = true;
|
||||
toastInfo.value.msg = `您已加入 "${v.roomId}" 房间`;
|
||||
}
|
||||
});
|
||||
|
||||
const memberwithoutOwner = computed(() =>
|
||||
members.value.filter((item) => item.id !== selfInfo.value.socketId)
|
||||
);
|
||||
|
||||
const selectDevice = computed(() => ({
|
||||
audioInput: currentAudioInput.value,
|
||||
audioOutput: currentAudioOutput.value,
|
||||
}));
|
||||
|
||||
// 当前激活的 菜单
|
||||
const activeControlMenu = computed<string[]>(() => {
|
||||
const arr = [];
|
||||
if (videoEnabled.value) arr.push('camera');
|
||||
if (audioEnabled.value) arr.push('audio');
|
||||
return arr;
|
||||
});
|
||||
|
||||
// 切换菜单按钮
|
||||
const handleMenuChange = (active: string) => {
|
||||
if (active === 'camera' || active === 'audio') {
|
||||
const enabled =
|
||||
active === 'camera' ? videoEnabled.value : audioEnabled.value;
|
||||
switchTrackEnable(active === 'camera' ? 'video' : 'audio', !enabled);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换扬声器、麦克风
|
||||
const deviceChange = (device: MediaDeviceInfo) => {
|
||||
if (device.kind === 'audioinput') {
|
||||
audioInputDevice.value = device;
|
||||
}
|
||||
|
||||
if (device.kind === 'audiooutput') {
|
||||
speakerRef.value = device.deviceId;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full">
|
||||
<div class="flex flex-1 flex-col overflow-hidden">
|
||||
<video
|
||||
id="video-call"
|
||||
ref="video"
|
||||
style="
|
||||
height: calc(100% - 80px);
|
||||
width: 100%;
|
||||
background-color: #1f1f1f;
|
||||
"
|
||||
playsinline
|
||||
autoplay
|
||||
></video>
|
||||
<VideoControl
|
||||
:selectDevice="selectDevice"
|
||||
:menuDisabled="!mediaLoaded"
|
||||
:activeControlMenu="activeControlMenu"
|
||||
@control-menu-change="handleMenuChange"
|
||||
@select-device="deviceChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-show="open" class="w-[400px] overflow-auto">
|
||||
<div class="member-video mt-4 px-2">
|
||||
<video
|
||||
v-for="item in memberwithoutOwner"
|
||||
:key="item.id"
|
||||
:ref="(ref) => collectOtherVideoRef(ref, item.id || '')"
|
||||
class="mb-4 h-[200px] w-full"
|
||||
style="background-color: #1f1f1f"
|
||||
playsinline
|
||||
autoplay
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ToastBox
|
||||
v-model:visible="toastInfo.visible"
|
||||
horizontal="center"
|
||||
vertical="top"
|
||||
type="success"
|
||||
:msg="toastInfo.msg"
|
||||
/>
|
||||
</template>
|
56
client/packages/rtc-web/src/views/video/video.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script lang="ts" setup>
|
||||
import { useCreateRoom } from '@/hooks/useRoom';
|
||||
import { BackPreviousLevel, BackTitle } from '@/components/back';
|
||||
import VideoRoom from './video-room.vue';
|
||||
import { useNow, useDateFormat } from '@vueuse/core';
|
||||
import { resetUrl } from '@/utils';
|
||||
import ErrorBoundary from '@/components/error-boundary/index.vue';
|
||||
import { useErrorCaptured } from '@/hooks/useErrorCaptured';
|
||||
|
||||
defineOptions({
|
||||
name: 'VideoView',
|
||||
});
|
||||
|
||||
const { roomId } = useCreateRoom('video', false);
|
||||
|
||||
const handleBackLevel = () => resetUrl();
|
||||
|
||||
const formatted = useDateFormat(useNow(), 'YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
const errors = useErrorCaptured();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full flex-col">
|
||||
<BackPreviousLevel
|
||||
class="py-2 shadow-md dark:shadow-sm dark:shadow-neutral-600"
|
||||
@back="handleBackLevel"
|
||||
>
|
||||
<BackTitle svg-name="chat" class="flex-1 justify-between">
|
||||
<div class="cursor-pointer">
|
||||
{{ roomId }}
|
||||
</div>
|
||||
</BackTitle>
|
||||
<div class="time pr-4 font-bold">{{ formatted }}</div>
|
||||
</BackPreviousLevel>
|
||||
|
||||
<div class="min-h-0 flex-1">
|
||||
<Suspense>
|
||||
<VideoRoom />
|
||||
<template #fallback>
|
||||
<ErrorBoundary
|
||||
v-if="errors.length"
|
||||
:tips-list="[
|
||||
'请检查网络情况',
|
||||
'允许浏览器打开摄像头、麦克风权限',
|
||||
'清空浏览器缓存重新打开页面',
|
||||
]"
|
||||
/>
|
||||
<div v-else class="flex h-full w-full items-center justify-center">
|
||||
<span class="loading loading-dots loading-lg"></span>
|
||||
</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@@ -5,7 +5,7 @@ defineOptions({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>Welcome</div>
|
||||
<div class="flex items-center justify-center">Welcome</div>
|
||||
</template>
|
||||
|
||||
<stylescoped></stylescoped>
|
||||
|
@@ -4,12 +4,28 @@ import eslintPlugin from 'vite-plugin-eslint';
|
||||
import { resolve } from 'path';
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
|
||||
|
||||
const pathResolve = (path: string) => resolve(__dirname, path);
|
||||
|
||||
// http://localhost:9092/api
|
||||
// https://im.iamtsm.cn/api
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: resolve(__dirname, '../../../client_dist/rtc-web'),
|
||||
minify: 'terser',
|
||||
emptyOutDir: true,
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: true,
|
||||
drop_debugger: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
@@ -18,6 +34,21 @@ export default defineConfig({
|
||||
},
|
||||
],
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'https://192.168.1.11:9092/api',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
secure: false,
|
||||
},
|
||||
},
|
||||
https: {
|
||||
key: fs.readFileSync('./src/keys/server.key'),
|
||||
cert: fs.readFileSync('./src/keys/server.crt'),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
@@ -27,5 +58,9 @@ export default defineConfig({
|
||||
// 指定symbolId格式
|
||||
symbolId: 'icon-[dir]-[name]',
|
||||
}),
|
||||
// basicSsl(),
|
||||
// mkcert({
|
||||
// source: 'coding',
|
||||
// }),
|
||||
],
|
||||
});
|
||||
|
357
client/pnpm-lock.yaml
generated
@@ -11,8 +11,10 @@ importers:
|
||||
'@vitejs/plugin-vue': ^4.2.3
|
||||
'@vitejs/plugin-vue-jsx': ^3.0.1
|
||||
'@vueuse/core': ^10.2.0
|
||||
'@vueuse/router': ^10.2.1
|
||||
autoprefixer: ^10.4.14
|
||||
daisyui: ^3.1.6
|
||||
dayjs: ^1.11.9
|
||||
eslint: ^8.43.0
|
||||
eslint-config-prettier: ^8.8.0
|
||||
eslint-plugin-prettier: ^4.2.1
|
||||
@@ -23,8 +25,11 @@ importers:
|
||||
postcss-import: ^15.1.0
|
||||
prettier: ^2.8.8
|
||||
prettier-plugin-tailwindcss: ^0.3.0
|
||||
socket.io-client: 2.3.0
|
||||
tailwindcss: ^3.3.2
|
||||
terser: ^5.19.4
|
||||
typescript: ^5.1.3
|
||||
vconsole: ^3.15.1
|
||||
vite: ^4.3.9
|
||||
vite-plugin-eslint: ^1.8.1
|
||||
vite-plugin-svg-icons: ^2.0.1
|
||||
@@ -32,14 +37,21 @@ importers:
|
||||
vue-eslint-parser: ^9.3.1
|
||||
vue-router: '4'
|
||||
vue-tsc: ^1.8.1
|
||||
vue3-emoji-picker: ^1.1.7
|
||||
vue3-popper: ^1.5.0
|
||||
dependencies:
|
||||
'@types/lodash': 4.14.195
|
||||
'@vitejs/plugin-vue-jsx': 3.0.1_vite@4.3.9+vue@3.3.4
|
||||
'@vueuse/core': 10.2.0_vue@3.3.4
|
||||
'@vueuse/router': 10.2.1_vue-router@4.2.2+vue@3.3.4
|
||||
dayjs: 1.11.9
|
||||
lodash: 4.17.21
|
||||
nanoid: 4.0.2
|
||||
socket.io-client: 2.3.0
|
||||
vue: 3.3.4
|
||||
vue-router: 4.2.2_vue@3.3.4
|
||||
vue3-emoji-picker: 1.1.7
|
||||
vue3-popper: 1.5.0_vue@3.3.4
|
||||
devDependencies:
|
||||
'@types/node': 20.3.2
|
||||
'@typescript-eslint/eslint-plugin': 5.60.1_emhilanx7rvvqwlixwztbx5x2m
|
||||
@@ -56,8 +68,10 @@ importers:
|
||||
prettier: 2.8.8
|
||||
prettier-plugin-tailwindcss: 0.3.0_prettier@2.8.8
|
||||
tailwindcss: 3.3.2
|
||||
terser: 5.19.4
|
||||
typescript: 5.1.3
|
||||
vite: 4.3.9_@types+node@20.3.2
|
||||
vconsole: 3.15.1
|
||||
vite: 4.3.9_66pwffvb2axzwdhxmcirxliemu
|
||||
vite-plugin-eslint: 1.8.1_eslint@8.43.0+vite@4.3.9
|
||||
vite-plugin-svg-icons: 2.0.1_vite@4.3.9
|
||||
vue-eslint-parser: 9.3.1_eslint@8.43.0
|
||||
@@ -336,6 +350,13 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@babel/runtime/7.22.15:
|
||||
resolution: {integrity: sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.0
|
||||
dev: true
|
||||
|
||||
/@babel/template/7.22.5:
|
||||
resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -620,6 +641,12 @@ packages:
|
||||
resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
/@jridgewell/source-map/0.3.5:
|
||||
resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==}
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.3
|
||||
'@jridgewell/trace-mapping': 0.3.18
|
||||
|
||||
/@jridgewell/sourcemap-codec/1.4.14:
|
||||
resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
|
||||
|
||||
@@ -653,6 +680,10 @@ packages:
|
||||
fastq: 1.15.0
|
||||
dev: true
|
||||
|
||||
/@popperjs/core/2.11.8:
|
||||
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
||||
dev: false
|
||||
|
||||
/@rollup/pluginutils/4.2.1:
|
||||
resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
@@ -842,7 +873,7 @@ packages:
|
||||
'@babel/core': 7.22.5
|
||||
'@babel/plugin-transform-typescript': 7.22.5_@babel+core@7.22.5
|
||||
'@vue/babel-plugin-jsx': 1.1.4_@babel+core@7.22.5
|
||||
vite: 4.3.9_@types+node@20.3.2
|
||||
vite: 4.3.9_66pwffvb2axzwdhxmcirxliemu
|
||||
vue: 3.3.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -855,7 +886,7 @@ packages:
|
||||
vite: ^4.0.0
|
||||
vue: ^3.2.25
|
||||
dependencies:
|
||||
vite: 4.3.9_@types+node@20.3.2
|
||||
vite: 4.3.9_66pwffvb2axzwdhxmcirxliemu
|
||||
vue: 3.3.4
|
||||
dev: true
|
||||
|
||||
@@ -1021,6 +1052,19 @@ packages:
|
||||
resolution: {integrity: sha512-IR7Mkq6QSgZ38q/2ZzOt+Zz1OpcEsnwE64WBumDQ+RGKrosFCtUA2zgRrOqDEzPBXrVB+4HhFkwDjQMu0fDBKw==}
|
||||
dev: false
|
||||
|
||||
/@vueuse/router/10.2.1_vue-router@4.2.2+vue@3.3.4:
|
||||
resolution: {integrity: sha512-H/1T4fLzMmeBNEmcXlbqk6AEp0HQpzf+0eeNJ6fGrs3RWClE2i3nYEFbtxfQeSm/7nZ6nf/UhgahzUQdyMhIwQ==}
|
||||
peerDependencies:
|
||||
vue-router: '>=4.0.0-rc.1'
|
||||
dependencies:
|
||||
'@vueuse/shared': 10.2.1_vue@3.3.4
|
||||
vue-demi: 0.14.5_vue@3.3.4
|
||||
vue-router: 4.2.2_vue@3.3.4
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
dev: false
|
||||
|
||||
/@vueuse/shared/10.2.0_vue@3.3.4:
|
||||
resolution: {integrity: sha512-dIeA8+g9Av3H5iF4NXR/sft4V6vys76CpZ6hxwj8eMXybXk2WRl3scSsOVi+kQ9SX38COR7AH7WwY83UcuxbSg==}
|
||||
dependencies:
|
||||
@@ -1030,6 +1074,15 @@ packages:
|
||||
- vue
|
||||
dev: false
|
||||
|
||||
/@vueuse/shared/10.2.1_vue@3.3.4:
|
||||
resolution: {integrity: sha512-QWHq2bSuGptkcxx4f4M/fBYC3Y8d3M2UYyLsyzoPgEoVzJURQ0oJeWXu79OiLlBb8gTKkqe4mO85T/sf39mmiw==}
|
||||
dependencies:
|
||||
vue-demi: 0.14.5_vue@3.3.4
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
dev: false
|
||||
|
||||
/acorn-jsx/5.3.2_acorn@8.9.0:
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
peerDependencies:
|
||||
@@ -1042,7 +1095,10 @@ packages:
|
||||
resolution: {integrity: sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/after/0.8.2:
|
||||
resolution: {integrity: sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==}
|
||||
dev: false
|
||||
|
||||
/ajv/6.12.6:
|
||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||
@@ -1127,11 +1183,19 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/arraybuffer.slice/0.0.7:
|
||||
resolution: {integrity: sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==}
|
||||
dev: false
|
||||
|
||||
/assign-symbols/1.0.0:
|
||||
resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/async-limiter/1.0.1:
|
||||
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
|
||||
dev: false
|
||||
|
||||
/atob/2.1.2:
|
||||
resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==}
|
||||
engines: {node: '>= 4.5.0'}
|
||||
@@ -1154,6 +1218,10 @@ packages:
|
||||
postcss-value-parser: 4.2.0
|
||||
dev: true
|
||||
|
||||
/backo2/1.0.2:
|
||||
resolution: {integrity: sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==}
|
||||
dev: false
|
||||
|
||||
/balanced-match/1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
dev: true
|
||||
@@ -1171,6 +1239,22 @@ packages:
|
||||
pascalcase: 0.1.1
|
||||
dev: true
|
||||
|
||||
/base64-arraybuffer/0.1.4:
|
||||
resolution: {integrity: sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
dev: false
|
||||
|
||||
/base64-arraybuffer/0.1.5:
|
||||
resolution: {integrity: sha512-437oANT9tP582zZMwSvZGy2nmSeAb8DW2me3y+Uv1Wp2Rulr8Mqlyrv3E7MLxmsiaPSMMDmiDVzgE+e8zlMx9g==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
dev: false
|
||||
|
||||
/better-assert/1.0.2:
|
||||
resolution: {integrity: sha512-bYeph2DFlpK1XmGs6fvlLRUN29QISM3GBuUwSFsMY2XRx4AvC0WNCS57j4c/xGrK2RS24C1w3YoBOsw9fT46tQ==}
|
||||
dependencies:
|
||||
callsite: 1.0.0
|
||||
dev: false
|
||||
|
||||
/big.js/5.2.2:
|
||||
resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
|
||||
dev: true
|
||||
@@ -1180,6 +1264,10 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/blob/0.0.5:
|
||||
resolution: {integrity: sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==}
|
||||
dev: false
|
||||
|
||||
/bluebird/3.7.2:
|
||||
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
|
||||
dev: true
|
||||
@@ -1236,6 +1324,9 @@ packages:
|
||||
node-releases: 2.0.12
|
||||
update-browserslist-db: 1.0.11_browserslist@4.21.9
|
||||
|
||||
/buffer-from/1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
/cache-base/1.0.1:
|
||||
resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1251,6 +1342,10 @@ packages:
|
||||
unset-value: 1.0.0
|
||||
dev: true
|
||||
|
||||
/callsite/1.0.0:
|
||||
resolution: {integrity: sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==}
|
||||
dev: false
|
||||
|
||||
/callsites/3.1.0:
|
||||
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1360,6 +1455,9 @@ packages:
|
||||
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
|
||||
dev: true
|
||||
|
||||
/commander/2.20.3:
|
||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||
|
||||
/commander/4.1.1:
|
||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -1370,9 +1468,20 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
dev: true
|
||||
|
||||
/component-bind/1.0.0:
|
||||
resolution: {integrity: sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw==}
|
||||
dev: false
|
||||
|
||||
/component-emitter/1.2.1:
|
||||
resolution: {integrity: sha512-jPatnhd33viNplKjqXKRkGU345p263OIWzDL2wH3LGIGp5Kojo+uXizHmOADRvhGFFTnJqX3jBAKP6vvmSDKcA==}
|
||||
dev: false
|
||||
|
||||
/component-emitter/1.3.0:
|
||||
resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==}
|
||||
dev: true
|
||||
|
||||
/component-inherit/0.0.3:
|
||||
resolution: {integrity: sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==}
|
||||
dev: false
|
||||
|
||||
/concat-map/0.0.1:
|
||||
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
|
||||
@@ -1387,6 +1496,16 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/copy-text-to-clipboard/3.2.0:
|
||||
resolution: {integrity: sha512-RnJFp1XR/LOBDckxTib5Qjr/PMfkatD0MUCQgdpqS8MdKiNUzBjAQBEN6oUy+jW7LI93BBG3DtMB2KOOKpGs2Q==}
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/core-js/3.32.1:
|
||||
resolution: {integrity: sha512-lqufgNn9NLnESg5mQeYsxQP5w7wrViSj0jr/kv6ECQiByzQkrn1MKvV0L3acttpDqfQrHLwr2KCMgX5b8X+lyQ==}
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
|
||||
/cors/2.8.5:
|
||||
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@@ -1465,10 +1584,18 @@ packages:
|
||||
- ts-node
|
||||
dev: true
|
||||
|
||||
/dayjs/1.11.9:
|
||||
resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==}
|
||||
dev: false
|
||||
|
||||
/de-indent/1.0.2:
|
||||
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
|
||||
dev: true
|
||||
|
||||
/debounce/1.2.1:
|
||||
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
|
||||
dev: false
|
||||
|
||||
/debug/2.6.9:
|
||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||
peerDependencies:
|
||||
@@ -1480,6 +1607,29 @@ packages:
|
||||
ms: 2.0.0
|
||||
dev: true
|
||||
|
||||
/debug/3.1.0:
|
||||
resolution: {integrity: sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.0.0
|
||||
dev: false
|
||||
|
||||
/debug/4.1.1:
|
||||
resolution: {integrity: sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==}
|
||||
deprecated: Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
dev: false
|
||||
|
||||
/debug/4.3.4:
|
||||
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
@@ -1603,6 +1753,36 @@ packages:
|
||||
engines: {node: '>= 4'}
|
||||
dev: true
|
||||
|
||||
/engine.io-client/3.4.4:
|
||||
resolution: {integrity: sha512-iU4CRr38Fecj8HoZEnFtm2EiKGbYZcPn3cHxqNGl/tmdWRf60KhK+9vE0JeSjgnlS/0oynEfLgKbT9ALpim0sQ==}
|
||||
dependencies:
|
||||
component-emitter: 1.3.0
|
||||
component-inherit: 0.0.3
|
||||
debug: 3.1.0
|
||||
engine.io-parser: 2.2.1
|
||||
has-cors: 1.1.0
|
||||
indexof: 0.0.1
|
||||
parseqs: 0.0.6
|
||||
parseuri: 0.0.6
|
||||
ws: 6.1.4
|
||||
xmlhttprequest-ssl: 1.5.5
|
||||
yeast: 0.1.2
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
dev: false
|
||||
|
||||
/engine.io-parser/2.2.1:
|
||||
resolution: {integrity: sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==}
|
||||
dependencies:
|
||||
after: 0.8.2
|
||||
arraybuffer.slice: 0.0.7
|
||||
base64-arraybuffer: 0.1.4
|
||||
blob: 0.0.5
|
||||
has-binary2: 1.0.3
|
||||
dev: false
|
||||
|
||||
/entities/1.1.2:
|
||||
resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==}
|
||||
dev: true
|
||||
@@ -2068,6 +2248,16 @@ packages:
|
||||
ansi-regex: 2.1.1
|
||||
dev: true
|
||||
|
||||
/has-binary2/1.0.3:
|
||||
resolution: {integrity: sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==}
|
||||
dependencies:
|
||||
isarray: 2.0.1
|
||||
dev: false
|
||||
|
||||
/has-cors/1.1.0:
|
||||
resolution: {integrity: sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA==}
|
||||
dev: false
|
||||
|
||||
/has-flag/1.0.0:
|
||||
resolution: {integrity: sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2142,6 +2332,10 @@ packages:
|
||||
readable-stream: 3.6.2
|
||||
dev: true
|
||||
|
||||
/idb/7.1.1:
|
||||
resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==}
|
||||
dev: false
|
||||
|
||||
/ignore/5.2.4:
|
||||
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
|
||||
engines: {node: '>= 4'}
|
||||
@@ -2166,6 +2360,10 @@ packages:
|
||||
engines: {node: '>=0.8.19'}
|
||||
dev: true
|
||||
|
||||
/indexof/0.0.1:
|
||||
resolution: {integrity: sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==}
|
||||
dev: false
|
||||
|
||||
/inflight/1.0.6:
|
||||
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
||||
dependencies:
|
||||
@@ -2302,6 +2500,10 @@ packages:
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
dev: true
|
||||
|
||||
/isarray/2.0.1:
|
||||
resolution: {integrity: sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==}
|
||||
dev: false
|
||||
|
||||
/isexe/2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
dev: true
|
||||
@@ -2540,7 +2742,6 @@ packages:
|
||||
|
||||
/ms/2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
dev: true
|
||||
|
||||
/ms/2.1.2:
|
||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
||||
@@ -2549,6 +2750,10 @@ packages:
|
||||
resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==}
|
||||
dev: true
|
||||
|
||||
/mutation-observer/1.0.3:
|
||||
resolution: {integrity: sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA==}
|
||||
dev: true
|
||||
|
||||
/mz/2.7.0:
|
||||
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
|
||||
dependencies:
|
||||
@@ -2619,6 +2824,10 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/object-component/0.0.3:
|
||||
resolution: {integrity: sha512-S0sN3agnVh2SZNEIGc0N1X4Z5K0JeFbGBrnuZpsxuUh5XLF0BnvWkMjRXo/zGKLd/eghvNIKcx1pQkmUjXIyrA==}
|
||||
dev: false
|
||||
|
||||
/object-copy/0.1.0:
|
||||
resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2686,6 +2895,26 @@ packages:
|
||||
callsites: 3.1.0
|
||||
dev: true
|
||||
|
||||
/parseqs/0.0.5:
|
||||
resolution: {integrity: sha512-B3Nrjw2aL7aI4TDujOzfA4NsEc4u1lVcIRE0xesutH8kjeWF70uk+W5cBlIQx04zUH9NTBvuN36Y9xLRPK6Jjw==}
|
||||
dependencies:
|
||||
better-assert: 1.0.2
|
||||
dev: false
|
||||
|
||||
/parseqs/0.0.6:
|
||||
resolution: {integrity: sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==}
|
||||
dev: false
|
||||
|
||||
/parseuri/0.0.5:
|
||||
resolution: {integrity: sha512-ijhdxJu6l5Ru12jF0JvzXVPvsC+VibqeaExlNoMhWN6VQ79PGjkmc7oA4W1lp00sFkNyj0fx6ivPLdV51/UMog==}
|
||||
dependencies:
|
||||
better-assert: 1.0.2
|
||||
dev: false
|
||||
|
||||
/parseuri/0.0.6:
|
||||
resolution: {integrity: sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==}
|
||||
dev: false
|
||||
|
||||
/pascalcase/0.1.1:
|
||||
resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2976,6 +3205,10 @@ packages:
|
||||
picomatch: 2.3.1
|
||||
dev: true
|
||||
|
||||
/regenerator-runtime/0.14.0:
|
||||
resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==}
|
||||
dev: true
|
||||
|
||||
/regex-not/1.0.2:
|
||||
resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -3133,6 +3366,39 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/socket.io-client/2.3.0:
|
||||
resolution: {integrity: sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==}
|
||||
dependencies:
|
||||
backo2: 1.0.2
|
||||
base64-arraybuffer: 0.1.5
|
||||
component-bind: 1.0.0
|
||||
component-emitter: 1.2.1
|
||||
debug: 4.1.1
|
||||
engine.io-client: 3.4.4
|
||||
has-binary2: 1.0.3
|
||||
has-cors: 1.1.0
|
||||
indexof: 0.0.1
|
||||
object-component: 0.0.3
|
||||
parseqs: 0.0.5
|
||||
parseuri: 0.0.5
|
||||
socket.io-parser: 3.3.3
|
||||
to-array: 0.1.4
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
dev: false
|
||||
|
||||
/socket.io-parser/3.3.3:
|
||||
resolution: {integrity: sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg==}
|
||||
dependencies:
|
||||
component-emitter: 1.3.0
|
||||
debug: 3.1.0
|
||||
isarray: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/source-map-js/1.0.2:
|
||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -3148,6 +3414,12 @@ packages:
|
||||
urix: 0.1.0
|
||||
dev: true
|
||||
|
||||
/source-map-support/0.5.21:
|
||||
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
|
||||
dependencies:
|
||||
buffer-from: 1.1.2
|
||||
source-map: 0.6.1
|
||||
|
||||
/source-map-url/0.4.1:
|
||||
resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==}
|
||||
deprecated: See https://github.com/lydell/source-map-url#deprecated
|
||||
@@ -3161,7 +3433,6 @@ packages:
|
||||
/source-map/0.6.1:
|
||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/split-string/3.1.0:
|
||||
resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==}
|
||||
@@ -3328,6 +3599,16 @@ packages:
|
||||
- ts-node
|
||||
dev: true
|
||||
|
||||
/terser/5.19.4:
|
||||
resolution: {integrity: sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@jridgewell/source-map': 0.3.5
|
||||
acorn: 8.9.0
|
||||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
|
||||
/text-table/0.2.0:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
dev: true
|
||||
@@ -3345,6 +3626,10 @@ packages:
|
||||
any-promise: 1.3.0
|
||||
dev: true
|
||||
|
||||
/to-array/0.1.4:
|
||||
resolution: {integrity: sha512-LhVdShQD/4Mk4zXNroIQZJC+Ap3zgLcDuwEdcmLv9CCO73NWockQDwyUnW/m8VX/EElfL6FcYx7EeutN4HJA6A==}
|
||||
dev: false
|
||||
|
||||
/to-fast-properties/2.0.0:
|
||||
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -3479,6 +3764,15 @@ packages:
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: true
|
||||
|
||||
/vconsole/3.15.1:
|
||||
resolution: {integrity: sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==}
|
||||
dependencies:
|
||||
'@babel/runtime': 7.22.15
|
||||
copy-text-to-clipboard: 3.2.0
|
||||
core-js: 3.32.1
|
||||
mutation-observer: 1.0.3
|
||||
dev: true
|
||||
|
||||
/vite-plugin-eslint/1.8.1_eslint@8.43.0+vite@4.3.9:
|
||||
resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==}
|
||||
peerDependencies:
|
||||
@@ -3489,7 +3783,7 @@ packages:
|
||||
'@types/eslint': 8.40.2
|
||||
eslint: 8.43.0
|
||||
rollup: 2.79.1
|
||||
vite: 4.3.9_@types+node@20.3.2
|
||||
vite: 4.3.9_66pwffvb2axzwdhxmcirxliemu
|
||||
dev: true
|
||||
|
||||
/vite-plugin-svg-icons/2.0.1_vite@4.3.9:
|
||||
@@ -3505,12 +3799,12 @@ packages:
|
||||
pathe: 0.2.0
|
||||
svg-baker: 1.7.0
|
||||
svgo: 2.8.0
|
||||
vite: 4.3.9_@types+node@20.3.2
|
||||
vite: 4.3.9_66pwffvb2axzwdhxmcirxliemu
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/vite/4.3.9_@types+node@20.3.2:
|
||||
/vite/4.3.9_66pwffvb2axzwdhxmcirxliemu:
|
||||
resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
hasBin: true
|
||||
@@ -3539,6 +3833,7 @@ packages:
|
||||
esbuild: 0.17.19
|
||||
postcss: 8.4.24
|
||||
rollup: 3.25.3
|
||||
terser: 5.19.4
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
@@ -3612,6 +3907,25 @@ packages:
|
||||
'@vue/server-renderer': 3.3.4_vue@3.3.4
|
||||
'@vue/shared': 3.3.4
|
||||
|
||||
/vue3-emoji-picker/1.1.7:
|
||||
resolution: {integrity: sha512-dKSI1NyrinYFykllwcOqBB1sw7EHdwQG4tjHYSO+khQkY8Csn4Evn5X2nAdz8Kl8o3P1J0jV4BGwbQ2dVWCxMA==}
|
||||
dependencies:
|
||||
'@popperjs/core': 2.11.8
|
||||
idb: 7.1.1
|
||||
vue: 3.3.4
|
||||
dev: false
|
||||
|
||||
/vue3-popper/1.5.0_vue@3.3.4:
|
||||
resolution: {integrity: sha512-xaEnx90YBnlSg5G2yWqm2DHWHg+DB99UVRp4VsyTF0QLXyHrqSuE1Xo5+sG0AQq/lBcrGMlk5NU5xE2MDLKViw==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
vue: ^3.2.20
|
||||
dependencies:
|
||||
'@popperjs/core': 2.11.8
|
||||
debounce: 1.2.1
|
||||
vue: 3.3.4
|
||||
dev: false
|
||||
|
||||
/which/2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -3629,11 +3943,30 @@ packages:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
dev: true
|
||||
|
||||
/ws/6.1.4:
|
||||
resolution: {integrity: sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: ^5.0.2
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
dependencies:
|
||||
async-limiter: 1.0.1
|
||||
dev: false
|
||||
|
||||
/xml-name-validator/4.0.0:
|
||||
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/xmlhttprequest-ssl/1.5.5:
|
||||
resolution: {integrity: sha512-/bFPLUgJrfGUL10AIv4Y7/CUt6so9CLtB/oFxQSHseSDNNCdC6vwwKEqwLN6wNPBg9YWXAiMu8jkf6RPRS/75Q==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: false
|
||||
|
||||
/yallist/3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
dev: false
|
||||
@@ -3647,6 +3980,10 @@ packages:
|
||||
engines: {node: '>= 14'}
|
||||
dev: true
|
||||
|
||||
/yeast/0.1.2:
|
||||
resolution: {integrity: sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==}
|
||||
dev: false
|
||||
|
||||
/yocto-queue/0.1.0:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|