Browse Source

fix: 优化

shuisheng 1 năm trước cách đây
mục cha
commit
dc46a98766

+ 1 - 1
package.json

@@ -63,7 +63,7 @@
     "socket.io-msgpack-parser": "^3.0.2",
     "spark-md5": "^3.0.2",
     "video.js": "^8.3.0",
-    "vue": "^3.3.4",
+    "vue": "^3.5.13",
     "vue-demi": "^0.13.11",
     "vue-i18n": "9",
     "vue-lazyload": "^3.0.0",

+ 209 - 89
pnpm-lock.yaml

@@ -13,7 +13,7 @@ importers:
         version: 0.12.0
       '@vueuse/core':
         specifier: ^10.11.1
-        version: 10.11.1(vue@3.3.4)
+        version: 10.11.1(vue@3.5.13(typescript@5.1.6))
       '@webav/av-cliper':
         specifier: ^1.0.6
         version: 1.0.6
@@ -52,7 +52,7 @@ importers:
         version: 3.10.1
       md-editor-v3:
         specifier: ^4.21.3
-        version: 4.21.3(@codemirror/view@6.34.2)(@lezer/common@1.2.3)(vue@3.3.4)
+        version: 4.21.3(@codemirror/view@6.34.2)(@lezer/common@1.2.3)(vue@3.5.13(typescript@5.1.6))
       mp4box:
         specifier: ^0.5.2
         version: 0.5.2
@@ -61,13 +61,13 @@ importers:
         version: 1.7.3
       naive-ui:
         specifier: ^2.40.2
-        version: 2.40.2(vue@3.3.4)
+        version: 2.40.2(vue@3.5.13(typescript@5.1.6))
       pinia:
         specifier: ^2.0.33
-        version: 2.0.33(typescript@5.1.6)(vue@3.3.4)
+        version: 2.0.33(typescript@5.1.6)(vue@3.5.13(typescript@5.1.6))
       pinia-plugin-persistedstate:
         specifier: ^3.2.0
-        version: 3.2.0(pinia@2.0.33(typescript@5.1.6)(vue@3.3.4))
+        version: 3.2.0(pinia@2.0.33(typescript@5.1.6)(vue@3.5.13(typescript@5.1.6)))
       qiniu-js:
         specifier: ^3.4.2
         version: 3.4.2
@@ -87,20 +87,20 @@ importers:
         specifier: ^8.3.0
         version: 8.3.0
       vue:
-        specifier: ^3.3.4
-        version: 3.3.4
+        specifier: ^3.5.13
+        version: 3.5.13(typescript@5.1.6)
       vue-demi:
         specifier: ^0.13.11
-        version: 0.13.11(vue@3.3.4)
+        version: 0.13.11(vue@3.5.13(typescript@5.1.6))
       vue-i18n:
         specifier: '9'
-        version: 9.9.1(vue@3.3.4)
+        version: 9.9.1(vue@3.5.13(typescript@5.1.6))
       vue-lazyload:
         specifier: ^3.0.0
         version: 3.0.0
       vue-router:
         specifier: ^4.0.13
-        version: 4.1.6(vue@3.3.4)
+        version: 4.1.6(vue@3.5.13(typescript@5.1.6))
       webrtc-adapter:
         specifier: ^8.2.2
         version: 8.2.2
@@ -275,13 +275,13 @@ importers:
         version: 5.1.6
       unplugin-vue-components:
         specifier: ^0.24.1
-        version: 0.24.1(@babel/parser@7.21.3)(rollup@2.79.1)(vue@3.3.4)
+        version: 0.24.1(@babel/parser@7.26.3)(rollup@2.79.1)(vue@3.5.13(typescript@5.1.6))
       vconsole:
         specifier: ^3.15.0
         version: 3.15.0
       vue-loader:
         specifier: ^17.2.2
-        version: 17.2.2(@vue/compiler-sfc@3.3.4)(vue@3.3.4)(webpack@5.76.2(@swc/core@1.3.84)(esbuild@0.15.18)(webpack-cli@4.10.0))
+        version: 17.2.2(@vue/compiler-sfc@3.3.4)(vue@3.5.13(typescript@5.1.6))(webpack@5.76.2(@swc/core@1.3.84)(esbuild@0.15.18)(webpack-cli@4.10.0))
       vue-style-loader:
         specifier: ^4.1.3
         version: 4.1.3
@@ -425,10 +425,18 @@ packages:
     resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==}
     engines: {node: '>=6.9.0'}
 
+  '@babel/helper-string-parser@7.25.9':
+    resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
+    engines: {node: '>=6.9.0'}
+
   '@babel/helper-validator-identifier@7.19.1':
     resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==}
     engines: {node: '>=6.9.0'}
 
+  '@babel/helper-validator-identifier@7.25.9':
+    resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
+    engines: {node: '>=6.9.0'}
+
   '@babel/helper-validator-option@7.21.0':
     resolution: {integrity: sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==}
     engines: {node: '>=6.9.0'}
@@ -450,6 +458,11 @@ packages:
     engines: {node: '>=6.0.0'}
     hasBin: true
 
+  '@babel/parser@7.26.3':
+    resolution: {integrity: sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==}
+    engines: {node: '>=6.0.0'}
+    hasBin: true
+
   '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6':
     resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==}
     engines: {node: '>=6.9.0'}
@@ -871,6 +884,10 @@ packages:
     resolution: {integrity: sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==}
     engines: {node: '>=6.9.0'}
 
+  '@babel/types@7.26.3':
+    resolution: {integrity: sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==}
+    engines: {node: '>=6.9.0'}
+
   '@codemirror/autocomplete@6.18.2':
     resolution: {integrity: sha512-wJGylKtMFR/Ds6Gh01+OovXE/pncPiKZNNBKuC39pKnH+XK5d9+WsNqcrdxPjFPFTigRBqse0rfxw9UxrfyhPg==}
     peerDependencies:
@@ -1513,6 +1530,9 @@ packages:
   '@jridgewell/sourcemap-codec@1.4.14':
     resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
 
+  '@jridgewell/sourcemap-codec@1.5.0':
+    resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
+
   '@jridgewell/trace-mapping@0.3.17':
     resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==}
 
@@ -2026,15 +2046,27 @@ packages:
   '@vue/compiler-core@3.3.4':
     resolution: {integrity: sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==}
 
+  '@vue/compiler-core@3.5.13':
+    resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==}
+
   '@vue/compiler-dom@3.3.4':
     resolution: {integrity: sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==}
 
+  '@vue/compiler-dom@3.5.13':
+    resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==}
+
   '@vue/compiler-sfc@3.3.4':
     resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==}
 
+  '@vue/compiler-sfc@3.5.13':
+    resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==}
+
   '@vue/compiler-ssr@3.3.4':
     resolution: {integrity: sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==}
 
+  '@vue/compiler-ssr@3.5.13':
+    resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==}
+
   '@vue/devtools-api@6.5.0':
     resolution: {integrity: sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==}
 
@@ -2065,23 +2097,26 @@ packages:
   '@vue/reactivity-transform@3.3.4':
     resolution: {integrity: sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==}
 
-  '@vue/reactivity@3.3.4':
-    resolution: {integrity: sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==}
+  '@vue/reactivity@3.5.13':
+    resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==}
 
-  '@vue/runtime-core@3.3.4':
-    resolution: {integrity: sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==}
+  '@vue/runtime-core@3.5.13':
+    resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==}
 
-  '@vue/runtime-dom@3.3.4':
-    resolution: {integrity: sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==}
+  '@vue/runtime-dom@3.5.13':
+    resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==}
 
-  '@vue/server-renderer@3.3.4':
-    resolution: {integrity: sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==}
+  '@vue/server-renderer@3.5.13':
+    resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==}
     peerDependencies:
-      vue: 3.3.4
+      vue: 3.5.13
 
   '@vue/shared@3.3.4':
     resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==}
 
+  '@vue/shared@3.5.13':
+    resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
+
   '@vueuse/core@10.11.1':
     resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==}
 
@@ -3091,9 +3126,6 @@ packages:
   csstype@3.0.11:
     resolution: {integrity: sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==}
 
-  csstype@3.1.2:
-    resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
-
   csstype@3.1.3:
     resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
 
@@ -4832,6 +4864,9 @@ packages:
     resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==}
     engines: {node: '>=12'}
 
+  magic-string@0.30.15:
+    resolution: {integrity: sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==}
+
   make-dir@3.1.0:
     resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
     engines: {node: '>=8'}
@@ -5077,6 +5112,11 @@ packages:
     engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
     hasBin: true
 
+  nanoid@3.3.8:
+    resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
+    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+    hasBin: true
+
   natural-compare-lite@1.4.0:
     resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==}
 
@@ -5402,6 +5442,9 @@ packages:
   picocolors@1.0.0:
     resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
 
+  picocolors@1.1.1:
+    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
   picomatch@2.3.1:
     resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
     engines: {node: '>=8.6'}
@@ -5844,6 +5887,10 @@ packages:
     resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==}
     engines: {node: ^10 || ^12 || >=14}
 
+  postcss@8.4.49:
+    resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
+    engines: {node: ^10 || ^12 || >=14}
+
   prelude-ls@1.2.1:
     resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
     engines: {node: '>= 0.8.0'}
@@ -6367,6 +6414,10 @@ packages:
     resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
     engines: {node: '>=0.10.0'}
 
+  source-map-js@1.2.1:
+    resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+    engines: {node: '>=0.10.0'}
+
   source-map-support@0.5.21:
     resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
 
@@ -7024,8 +7075,13 @@ packages:
   vue-style-loader@4.1.3:
     resolution: {integrity: sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==}
 
-  vue@3.3.4:
-    resolution: {integrity: sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==}
+  vue@3.5.13:
+    resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==}
+    peerDependencies:
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
 
   vueuc@0.4.64:
     resolution: {integrity: sha512-wlJQj7fIwKK2pOEoOq4Aro8JdPOGpX8aWQhV8YkTW9OgWD2uj2O8ANzvSsIGjx7LTOc7QbS7sXdxHi6XvRnHPA==}
@@ -7517,8 +7573,12 @@ snapshots:
 
   '@babel/helper-string-parser@7.19.4': {}
 
+  '@babel/helper-string-parser@7.25.9': {}
+
   '@babel/helper-validator-identifier@7.19.1': {}
 
+  '@babel/helper-validator-identifier@7.25.9': {}
+
   '@babel/helper-validator-option@7.21.0': {}
 
   '@babel/helper-wrap-function@7.20.5':
@@ -7548,6 +7608,10 @@ snapshots:
     dependencies:
       '@babel/types': 7.21.3
 
+  '@babel/parser@7.26.3':
+    dependencies:
+      '@babel/types': 7.26.3
+
   '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6(@babel/core@7.21.3)':
     dependencies:
       '@babel/core': 7.21.3
@@ -8094,6 +8158,11 @@ snapshots:
       '@babel/helper-validator-identifier': 7.19.1
       to-fast-properties: 2.0.0
 
+  '@babel/types@7.26.3':
+    dependencies:
+      '@babel/helper-string-parser': 7.25.9
+      '@babel/helper-validator-identifier': 7.25.9
+
   '@codemirror/autocomplete@6.18.2(@codemirror/language@6.10.3)(@codemirror/state@6.4.1)(@codemirror/view@6.34.2)(@lezer/common@1.2.3)':
     dependencies:
       '@codemirror/language': 6.10.3
@@ -8505,9 +8574,9 @@ snapshots:
     dependencies:
       css-render: 0.15.14
 
-  '@css-render/vue3-ssr@0.15.14(vue@3.3.4)':
+  '@css-render/vue3-ssr@0.15.14(vue@3.5.13(typescript@5.1.6))':
     dependencies:
-      vue: 3.3.4
+      vue: 3.5.13(typescript@5.1.6)
 
   '@csstools/postcss-cascade-layers@1.1.1(postcss@8.4.21)':
     dependencies:
@@ -8806,6 +8875,8 @@ snapshots:
 
   '@jridgewell/sourcemap-codec@1.4.14': {}
 
+  '@jridgewell/sourcemap-codec@1.5.0': {}
+
   '@jridgewell/trace-mapping@0.3.17':
     dependencies:
       '@jridgewell/resolve-uri': 3.1.0
@@ -9367,11 +9438,24 @@ snapshots:
       estree-walker: 2.0.2
       source-map-js: 1.0.2
 
+  '@vue/compiler-core@3.5.13':
+    dependencies:
+      '@babel/parser': 7.26.3
+      '@vue/shared': 3.5.13
+      entities: 4.5.0
+      estree-walker: 2.0.2
+      source-map-js: 1.2.1
+
   '@vue/compiler-dom@3.3.4':
     dependencies:
       '@vue/compiler-core': 3.3.4
       '@vue/shared': 3.3.4
 
+  '@vue/compiler-dom@3.5.13':
+    dependencies:
+      '@vue/compiler-core': 3.5.13
+      '@vue/shared': 3.5.13
+
   '@vue/compiler-sfc@3.3.4':
     dependencies:
       '@babel/parser': 7.21.3
@@ -9385,11 +9469,28 @@ snapshots:
       postcss: 8.4.21
       source-map-js: 1.0.2
 
+  '@vue/compiler-sfc@3.5.13':
+    dependencies:
+      '@babel/parser': 7.26.3
+      '@vue/compiler-core': 3.5.13
+      '@vue/compiler-dom': 3.5.13
+      '@vue/compiler-ssr': 3.5.13
+      '@vue/shared': 3.5.13
+      estree-walker: 2.0.2
+      magic-string: 0.30.15
+      postcss: 8.4.49
+      source-map-js: 1.2.1
+
   '@vue/compiler-ssr@3.3.4':
     dependencies:
       '@vue/compiler-dom': 3.3.4
       '@vue/shared': 3.3.4
 
+  '@vue/compiler-ssr@3.5.13':
+    dependencies:
+      '@vue/compiler-dom': 3.5.13
+      '@vue/shared': 3.5.13
+
   '@vue/devtools-api@6.5.0': {}
 
   '@vue/eslint-config-prettier@8.0.0(@types/eslint@8.21.2)(eslint@8.36.0)(prettier@3.0.1)':
@@ -9426,44 +9527,47 @@ snapshots:
       estree-walker: 2.0.2
       magic-string: 0.30.0
 
-  '@vue/reactivity@3.3.4':
+  '@vue/reactivity@3.5.13':
     dependencies:
-      '@vue/shared': 3.3.4
+      '@vue/shared': 3.5.13
 
-  '@vue/runtime-core@3.3.4':
+  '@vue/runtime-core@3.5.13':
     dependencies:
-      '@vue/reactivity': 3.3.4
-      '@vue/shared': 3.3.4
+      '@vue/reactivity': 3.5.13
+      '@vue/shared': 3.5.13
 
-  '@vue/runtime-dom@3.3.4':
+  '@vue/runtime-dom@3.5.13':
     dependencies:
-      '@vue/runtime-core': 3.3.4
-      '@vue/shared': 3.3.4
-      csstype: 3.1.2
+      '@vue/reactivity': 3.5.13
+      '@vue/runtime-core': 3.5.13
+      '@vue/shared': 3.5.13
+      csstype: 3.1.3
 
-  '@vue/server-renderer@3.3.4(vue@3.3.4)':
+  '@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.1.6))':
     dependencies:
-      '@vue/compiler-ssr': 3.3.4
-      '@vue/shared': 3.3.4
-      vue: 3.3.4
+      '@vue/compiler-ssr': 3.5.13
+      '@vue/shared': 3.5.13
+      vue: 3.5.13(typescript@5.1.6)
 
   '@vue/shared@3.3.4': {}
 
-  '@vueuse/core@10.11.1(vue@3.3.4)':
+  '@vue/shared@3.5.13': {}
+
+  '@vueuse/core@10.11.1(vue@3.5.13(typescript@5.1.6))':
     dependencies:
       '@types/web-bluetooth': 0.0.20
       '@vueuse/metadata': 10.11.1
-      '@vueuse/shared': 10.11.1(vue@3.3.4)
-      vue-demi: 0.14.10(vue@3.3.4)
+      '@vueuse/shared': 10.11.1(vue@3.5.13(typescript@5.1.6))
+      vue-demi: 0.14.10(vue@3.5.13(typescript@5.1.6))
     transitivePeerDependencies:
       - '@vue/composition-api'
       - vue
 
   '@vueuse/metadata@10.11.1': {}
 
-  '@vueuse/shared@10.11.1(vue@3.3.4)':
+  '@vueuse/shared@10.11.1(vue@3.5.13(typescript@5.1.6))':
     dependencies:
-      vue-demi: 0.14.10(vue@3.3.4)
+      vue-demi: 0.14.10(vue@3.5.13(typescript@5.1.6))
     transitivePeerDependencies:
       - '@vue/composition-api'
       - vue
@@ -10712,8 +10816,6 @@ snapshots:
 
   csstype@3.0.11: {}
 
-  csstype@3.1.2: {}
-
   csstype@3.1.3: {}
 
   cz-conventional-changelog@3.3.0(@swc/core@1.3.84):
@@ -12670,6 +12772,10 @@ snapshots:
     dependencies:
       '@jridgewell/sourcemap-codec': 1.4.14
 
+  magic-string@0.30.15:
+    dependencies:
+      '@jridgewell/sourcemap-codec': 1.5.0
+
   make-dir@3.1.0:
     dependencies:
       semver: 6.3.1
@@ -12697,7 +12803,7 @@ snapshots:
       punycode.js: 2.3.1
       uc.micro: 2.1.0
 
-  md-editor-v3@4.21.3(@codemirror/view@6.34.2)(@lezer/common@1.2.3)(vue@3.3.4):
+  md-editor-v3@4.21.3(@codemirror/view@6.34.2)(@lezer/common@1.2.3)(vue@3.5.13(typescript@5.1.6)):
     dependencies:
       '@codemirror/lang-markdown': 6.3.1
       '@codemirror/language-data': 6.5.1(@codemirror/view@6.34.2)
@@ -12711,7 +12817,7 @@ snapshots:
       markdown-it-sub: 2.0.0
       markdown-it-sup: 2.0.0
       medium-zoom: 1.1.0
-      vue: 3.3.4
+      vue: 3.5.13(typescript@5.1.6)
       xss: 1.0.15
     transitivePeerDependencies:
       - '@codemirror/view'
@@ -12889,10 +12995,10 @@ snapshots:
       object-assign: 4.1.1
       thenify-all: 1.6.0
 
-  naive-ui@2.40.2(vue@3.3.4):
+  naive-ui@2.40.2(vue@3.5.13(typescript@5.1.6)):
     dependencies:
       '@css-render/plugin-bem': 0.15.14(css-render@0.15.14)
-      '@css-render/vue3-ssr': 0.15.14(vue@3.3.4)
+      '@css-render/vue3-ssr': 0.15.14(vue@3.5.13(typescript@5.1.6))
       '@types/katex': 0.16.7
       '@types/lodash': 4.17.13
       '@types/lodash-es': 4.17.12
@@ -12907,10 +13013,10 @@ snapshots:
       lodash-es: 4.17.21
       seemly: 0.3.9
       treemate: 0.3.11
-      vdirs: 0.1.8(vue@3.3.4)
-      vooks: 0.2.12(vue@3.3.4)
-      vue: 3.3.4
-      vueuc: 0.4.64(vue@3.3.4)
+      vdirs: 0.1.8(vue@3.5.13(typescript@5.1.6))
+      vooks: 0.2.12(vue@3.5.13(typescript@5.1.6))
+      vue: 3.5.13(typescript@5.1.6)
+      vueuc: 0.4.64(vue@3.5.13(typescript@5.1.6))
 
   nan@2.17.0:
     optional: true
@@ -12920,6 +13026,8 @@ snapshots:
 
   nanoid@3.3.4: {}
 
+  nanoid@3.3.8: {}
+
   natural-compare-lite@1.4.0: {}
 
   natural-compare@1.4.0: {}
@@ -13246,6 +13354,8 @@ snapshots:
 
   picocolors@1.0.0: {}
 
+  picocolors@1.1.1: {}
+
   picomatch@2.3.1: {}
 
   pidtree@0.5.0: {}
@@ -13254,15 +13364,15 @@ snapshots:
 
   pify@3.0.0: {}
 
-  pinia-plugin-persistedstate@3.2.0(pinia@2.0.33(typescript@5.1.6)(vue@3.3.4)):
+  pinia-plugin-persistedstate@3.2.0(pinia@2.0.33(typescript@5.1.6)(vue@3.5.13(typescript@5.1.6))):
     dependencies:
-      pinia: 2.0.33(typescript@5.1.6)(vue@3.3.4)
+      pinia: 2.0.33(typescript@5.1.6)(vue@3.5.13(typescript@5.1.6))
 
-  pinia@2.0.33(typescript@5.1.6)(vue@3.3.4):
+  pinia@2.0.33(typescript@5.1.6)(vue@3.5.13(typescript@5.1.6)):
     dependencies:
       '@vue/devtools-api': 6.5.0
-      vue: 3.3.4
-      vue-demi: 0.13.11(vue@3.3.4)
+      vue: 3.5.13(typescript@5.1.6)
+      vue-demi: 0.13.11(vue@3.5.13(typescript@5.1.6))
     optionalDependencies:
       typescript: 5.1.6
 
@@ -13679,6 +13789,12 @@ snapshots:
       picocolors: 1.0.0
       source-map-js: 1.0.2
 
+  postcss@8.4.49:
+    dependencies:
+      nanoid: 3.3.8
+      picocolors: 1.1.1
+      source-map-js: 1.2.1
+
   prelude-ls@1.2.1: {}
 
   prettier-linter-helpers@1.0.0:
@@ -14273,6 +14389,8 @@ snapshots:
 
   source-map-js@1.0.2: {}
 
+  source-map-js@1.2.1: {}
+
   source-map-support@0.5.21:
     dependencies:
       buffer-from: 1.1.2
@@ -14782,7 +14900,7 @@ snapshots:
 
   unpipe@1.0.0: {}
 
-  unplugin-vue-components@0.24.1(@babel/parser@7.21.3)(rollup@2.79.1)(vue@3.3.4):
+  unplugin-vue-components@0.24.1(@babel/parser@7.26.3)(rollup@2.79.1)(vue@3.5.13(typescript@5.1.6)):
     dependencies:
       '@antfu/utils': 0.7.2
       '@rollup/pluginutils': 5.0.2(rollup@2.79.1)
@@ -14794,9 +14912,9 @@ snapshots:
       minimatch: 7.4.6
       resolve: 1.22.1
       unplugin: 1.3.1
-      vue: 3.3.4
+      vue: 3.5.13(typescript@5.1.6)
     optionalDependencies:
-      '@babel/parser': 7.21.3
+      '@babel/parser': 7.26.3
     transitivePeerDependencies:
       - rollup
       - supports-color
@@ -14885,10 +15003,10 @@ snapshots:
       core-js: 3.29.1
       mutation-observer: 1.0.3
 
-  vdirs@0.1.8(vue@3.3.4):
+  vdirs@0.1.8(vue@3.5.13(typescript@5.1.6)):
     dependencies:
       evtd: 0.2.4
-      vue: 3.3.4
+      vue: 3.5.13(typescript@5.1.6)
 
   verror@1.10.0:
     dependencies:
@@ -14936,18 +15054,18 @@ snapshots:
       sass: 1.59.3
       terser: 5.16.6
 
-  vooks@0.2.12(vue@3.3.4):
+  vooks@0.2.12(vue@3.5.13(typescript@5.1.6)):
     dependencies:
       evtd: 0.2.4
-      vue: 3.3.4
+      vue: 3.5.13(typescript@5.1.6)
 
-  vue-demi@0.13.11(vue@3.3.4):
+  vue-demi@0.13.11(vue@3.5.13(typescript@5.1.6)):
     dependencies:
-      vue: 3.3.4
+      vue: 3.5.13(typescript@5.1.6)
 
-  vue-demi@0.14.10(vue@3.3.4):
+  vue-demi@0.14.10(vue@3.5.13(typescript@5.1.6)):
     dependencies:
-      vue: 3.3.4
+      vue: 3.5.13(typescript@5.1.6)
 
   vue-eslint-parser@9.3.1(eslint@8.36.0):
     dependencies:
@@ -14962,16 +15080,16 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  vue-i18n@9.9.1(vue@3.3.4):
+  vue-i18n@9.9.1(vue@3.5.13(typescript@5.1.6)):
     dependencies:
       '@intlify/core-base': 9.9.1
       '@intlify/shared': 9.9.1
       '@vue/devtools-api': 6.5.0
-      vue: 3.3.4
+      vue: 3.5.13(typescript@5.1.6)
 
   vue-lazyload@3.0.0: {}
 
-  vue-loader@17.2.2(@vue/compiler-sfc@3.3.4)(vue@3.3.4)(webpack@5.76.2(@swc/core@1.3.84)(esbuild@0.15.18)(webpack-cli@4.10.0)):
+  vue-loader@17.2.2(@vue/compiler-sfc@3.3.4)(vue@3.5.13(typescript@5.1.6))(webpack@5.76.2(@swc/core@1.3.84)(esbuild@0.15.18)(webpack-cli@4.10.0)):
     dependencies:
       chalk: 4.1.2
       hash-sum: 2.0.0
@@ -14979,36 +15097,38 @@ snapshots:
       webpack: 5.76.2(@swc/core@1.3.84)(esbuild@0.15.18)(webpack-cli@4.10.0)
     optionalDependencies:
       '@vue/compiler-sfc': 3.3.4
-      vue: 3.3.4
+      vue: 3.5.13(typescript@5.1.6)
 
-  vue-router@4.1.6(vue@3.3.4):
+  vue-router@4.1.6(vue@3.5.13(typescript@5.1.6)):
     dependencies:
       '@vue/devtools-api': 6.5.0
-      vue: 3.3.4
+      vue: 3.5.13(typescript@5.1.6)
 
   vue-style-loader@4.1.3:
     dependencies:
       hash-sum: 1.0.2
       loader-utils: 1.4.2
 
-  vue@3.3.4:
+  vue@3.5.13(typescript@5.1.6):
     dependencies:
-      '@vue/compiler-dom': 3.3.4
-      '@vue/compiler-sfc': 3.3.4
-      '@vue/runtime-dom': 3.3.4
-      '@vue/server-renderer': 3.3.4(vue@3.3.4)
-      '@vue/shared': 3.3.4
+      '@vue/compiler-dom': 3.5.13
+      '@vue/compiler-sfc': 3.5.13
+      '@vue/runtime-dom': 3.5.13
+      '@vue/server-renderer': 3.5.13(vue@3.5.13(typescript@5.1.6))
+      '@vue/shared': 3.5.13
+    optionalDependencies:
+      typescript: 5.1.6
 
-  vueuc@0.4.64(vue@3.3.4):
+  vueuc@0.4.64(vue@3.5.13(typescript@5.1.6)):
     dependencies:
-      '@css-render/vue3-ssr': 0.15.14(vue@3.3.4)
+      '@css-render/vue3-ssr': 0.15.14(vue@3.5.13(typescript@5.1.6))
       '@juggle/resize-observer': 3.4.0
       css-render: 0.15.14
       evtd: 0.2.4
       seemly: 0.3.9
-      vdirs: 0.1.8(vue@3.3.4)
-      vooks: 0.2.12(vue@3.3.4)
-      vue: 3.3.4
+      vdirs: 0.1.8(vue@3.5.13(typescript@5.1.6))
+      vooks: 0.2.12(vue@3.5.13(typescript@5.1.6))
+      vue: 3.5.13(typescript@5.1.6)
 
   w3c-hr-time@1.0.2:
     dependencies:

+ 1 - 0
script/config/webpack.common.ts

@@ -402,6 +402,7 @@ const commonConfig = (isProduction) => {
         },
         __VUE_OPTIONS_API__: false,
         __VUE_PROD_DEVTOOLS__: false,
+        __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,
       }),
       // ts类型检查
       // feat: drop support for Vue.js:https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/pull/801

+ 3 - 0
src/App.vue

@@ -51,6 +51,9 @@ const themeOverrides: GlobalThemeOverrides = {
 };
 
 onMounted(() => {
+  document.addEventListener('click', () => {
+    appStore.pageIsClick = true;
+  });
   handleGlobalMsgMyList();
   useGoogleAd();
   initGlobalData();

+ 48 - 0
src/api/blacklist.ts

@@ -0,0 +1,48 @@
+import { IBlacklist } from '@/interface';
+import request from '@/utils/request';
+
+export function fetchBlacklistAddAdminDisable(data: IBlacklist) {
+  return request.post('/blacklist/add_admin_disable', data);
+}
+
+export function fetchBlacklistAddDisableMsg(data: IBlacklist) {
+  return request.post('/blacklist/add_disable_msg', data);
+}
+
+export function fetchBlacklistDelDisableMsg(data: IBlacklist) {
+  return request.post('/blacklist/del_disable_msg', data);
+}
+
+export function fetchBlacklistDelAdminDisable(data: IBlacklist) {
+  return request.post('/blacklist/del_admin_disable', data);
+}
+
+export function fetchBlacklistList(params) {
+  return request.instance({
+    url: '/blacklist/list',
+    method: 'get',
+    params,
+  });
+}
+
+export function fetchCreateBlacklist(data: IBlacklist) {
+  return request.instance({
+    url: '/blacklist/create',
+    method: 'post',
+    data,
+  });
+}
+
+export function fetchUpdateBlacklist(data: IBlacklist) {
+  return request.instance({
+    url: `/blacklist/update/${data.id!}`,
+    method: 'put',
+    data,
+  });
+}
+export function fetchDeleteBlacklist(id: number) {
+  return request.instance({
+    url: `/blacklist/delete/${id}`,
+    method: 'delete',
+  });
+}

+ 2 - 4
src/api/live.ts

@@ -7,10 +7,8 @@ export function fetchLiveList(params) {
   });
 }
 
-export function fetchLiveRoomOnlineUser(params) {
-  return request.get('/live/live_room_online_user', {
-    params,
-  });
+export function fetchLiveRoomOnlineUser(liveRoomId: number) {
+  return request.get(`/live/live_room_online_user/${liveRoomId}`);
 }
 
 export function fetchLiveLiveRoomIsLive(liveRoomId: number) {

+ 2 - 1
src/api/user.ts

@@ -36,10 +36,11 @@ export function fetchQrcodeLoginStatus({ platform, login_id }) {
   });
 }
 
-export function fetchUsernameLogin({ username, password }) {
+export function fetchUsernameLogin({ username, password, exp }) {
   return request.post('/user/username_login', {
     username,
     password,
+    exp,
   });
 }
 export function fetchIdLogin({ id, password }) {

+ 1 - 0
src/assets/constant.scss

@@ -11,6 +11,7 @@
 $layout-head-h: 60px;
 
 $video-ratio: calc(16 / 9);
+$w-1600: 1600px;
 $w-1500: 1500px;
 $w-1475: 1475px;
 $w-1450: 1450px;

BIN
src/assets/img/browse.png


BIN
src/assets/img/good.png


BIN
src/assets/img/share.png


+ 14 - 1
src/components/VideoControls/index.vue

@@ -136,7 +136,7 @@
                 :class="{ active: appStore.videoControls?.renderMode === item }"
                 v-for="item in LiveRenderEnum"
                 :key="item"
-                @click="appStore.videoControls.renderMode = item"
+                @click="changeRenderMode(item)"
               >
                 {{ item }}
               </div>
@@ -203,6 +203,7 @@ import { LiveLineEnum, LiveRenderEnum } from '@/interface';
 import { AppRootState, useAppStore } from '@/store/app';
 import { useCacheStore } from '@/store/cache';
 import { ILiveRoom, LiveRoomTypeEnum } from '@/types/ILiveRoom';
+import { isMSESupported } from '@/utils';
 
 const props = withDefaults(
   defineProps<{
@@ -247,6 +248,14 @@ onUnmounted(() => {
   window.removeEventListener('keydown', handleKeydown);
 });
 
+function changeRenderMode(item: LiveRenderEnum) {
+  if (item === LiveRenderEnum.canvas) {
+    window.$message.warning('暂不开放');
+    return;
+  }
+  appStore.videoControls.renderMode = item;
+}
+
 function handleKeydown(e) {
   if (e.key === 'Escape') {
     if (appStore.videoControlsValue.pageFullMode) {
@@ -278,6 +287,10 @@ function handlePageFull() {
 }
 
 function changeLiveLine(item: LiveLineEnum) {
+  if (item === LiveLineEnum.flv && !isMSESupported()) {
+    window.$message.warning('当前环境不支持该线路');
+    return;
+  }
   const type = props.liveRoom.type!;
   if (item === LiveLineEnum['rtmp-rtc']) {
     if (

+ 58 - 8
src/constant.ts

@@ -95,6 +95,41 @@ export const COMMON_URL = {
   },
 };
 
+export const DEFAULT_ROLE_INFO = {
+  ALL_ROLE: {
+    id: 1,
+    role_value: 'ALL_ROLE',
+  },
+  ADMIN: {
+    id: 2,
+    role_value: 'ADMIN',
+  },
+  SUPER_ADMIN: {
+    id: 3,
+    role_value: 'SUPER_ADMIN',
+  },
+  LIVE_ADMIN: {
+    id: 4,
+    role_value: 'LIVE_ADMIN',
+  },
+  USER: {
+    id: 5,
+    role_value: 'USER',
+  },
+  VIP_USER: {
+    id: 6,
+    role_value: 'VIP_USER',
+  },
+  SVIP_USER: {
+    id: 7,
+    role_value: 'SVIP_USER',
+  },
+  TOURIST_USER: {
+    id: 8,
+    role_value: 'TOURIST_USER',
+  },
+};
+
 export const DEFAULT_AUTH_INFO = {
   ALL_AUTH: {
     id: 1,
@@ -199,38 +234,53 @@ export const allMediaTypeList = {
   [MediaTypeEnum.camera]: {
     type: MediaTypeEnum.camera,
     txt: '摄像头',
+    priority: 1,
+  },
+  [MediaTypeEnum.cameraRemoveGreen]: {
+    type: MediaTypeEnum.cameraRemoveGreen,
+    txt: '摄像头(移除绿幕)',
+    priority: 2,
+  },
+  [MediaTypeEnum.media]: {
+    type: MediaTypeEnum.media,
+    txt: '视频',
+    priority: 3,
+  },
+  [MediaTypeEnum.mediaRemoveGreen]: {
+    type: MediaTypeEnum.mediaRemoveGreen,
+    txt: '视频(移除绿幕)',
+    priority: 4,
   },
   [MediaTypeEnum.microphone]: {
     type: MediaTypeEnum.microphone,
     txt: '麦克风',
+    priority: 5,
   },
   [MediaTypeEnum.screen]: {
     type: MediaTypeEnum.screen,
     txt: '窗口',
+    priority: 6,
   },
   [MediaTypeEnum.txt]: {
     type: MediaTypeEnum.txt,
     txt: '文字',
+    priority: 7,
   },
   [MediaTypeEnum.img]: {
     type: MediaTypeEnum.img,
     txt: '图片',
+    priority: 8,
   },
-  [MediaTypeEnum.media]: {
-    type: MediaTypeEnum.media,
-    txt: '视频',
-  },
+
   [MediaTypeEnum.time]: {
     type: MediaTypeEnum.time,
     txt: '时间',
+    priority: 9,
   },
   [MediaTypeEnum.stopwatch]: {
     type: MediaTypeEnum.stopwatch,
     txt: '秒表',
-  },
-  [MediaTypeEnum.removeGreenVideo]: {
-    type: MediaTypeEnum.removeGreenVideo,
-    txt: '移除绿幕',
+    priority: 10,
   },
 };
 

+ 8 - 2
src/hooks/use-play.ts

@@ -150,6 +150,10 @@ export function useFlvPlay() {
     () => appStore.playing,
     (newVal) => {
       if (flvVideoEl.value) {
+        if (appStore.pageIsClick) {
+          flvVideoEl.value.muted = false;
+          cacheStore.muted = false;
+        }
         if (newVal) {
           flvVideoEl.value.play();
         } else {
@@ -169,7 +173,6 @@ export function useFlvPlay() {
   watch(
     () => cacheStore.muted,
     (newVal) => {
-      appStore.pageIsClick = true;
       if (flvVideoEl.value) {
         flvVideoEl.value.muted = newVal;
       }
@@ -329,6 +332,10 @@ export function useHlsPlay() {
     () => appStore.playing,
     (newVal) => {
       if (hlsVideoEl.value) {
+        if (appStore.pageIsClick) {
+          hlsVideoEl.value.muted = false;
+          cacheStore.muted = false;
+        }
         if (newVal) {
           hlsVideoEl.value.play();
         } else {
@@ -348,7 +355,6 @@ export function useHlsPlay() {
   watch(
     () => cacheStore.muted,
     (newVal) => {
-      appStore.pageIsClick = true;
       if (hlsVideoEl.value) {
         hlsVideoEl.value.muted = newVal;
       }

+ 43 - 27
src/hooks/use-pull.ts

@@ -51,7 +51,7 @@ export function usePull() {
   const videoWrapRef = ref<HTMLDivElement>();
   const videoResolution = ref();
   const remoteVideo = ref<Array<HTMLVideoElement | HTMLCanvasElement>>([]);
-  const remoteStream = ref<MediaStream[]>([]);
+  const remoteStreamVideo = ref<HTMLVideoElement[]>([]);
   const {
     mySocketId,
     initWs,
@@ -79,6 +79,12 @@ export function usePull() {
     destroyHls();
   });
 
+  function stopPlay() {
+    handleRtmpToRtcStop();
+    destroyFlv();
+    destroyHls();
+  }
+
   function handleStopDrawing() {
     changeWrapSizeFn = undefined;
     stopDrawingArr.value.forEach((cb) => cb());
@@ -141,8 +147,13 @@ export function usePull() {
     () => hlsVideoEl.value,
     (newval) => {
       if (newval) {
-        // @ts-ignore
-        remoteStream.value.push(newval.captureStream());
+        newval.style.removeProperty('opacity');
+        newval.style.removeProperty('position');
+        newval.style.removeProperty('top');
+        newval.style.removeProperty('bottom');
+        newval.style.removeProperty('left');
+        newval.style.removeProperty('right');
+        remoteStreamVideo.value.push(newval);
       }
     }
   );
@@ -151,8 +162,13 @@ export function usePull() {
     () => flvVideoEl.value,
     (newval) => {
       if (newval) {
-        // @ts-ignore
-        remoteStream.value.push(newval.captureStream());
+        newval.style.removeProperty('opacity');
+        newval.style.removeProperty('position');
+        newval.style.removeProperty('top');
+        newval.style.removeProperty('bottom');
+        newval.style.removeProperty('left');
+        newval.style.removeProperty('right');
+        remoteStreamVideo.value.push(newval);
       }
     }
   );
@@ -165,13 +181,11 @@ export function usePull() {
   );
 
   watch(
-    [() => appStore.videoControls.renderMode, () => remoteStream.value],
+    [() => remoteStreamVideo.value, () => appStore.videoControls.renderMode],
     () => {
       handleStopDrawing();
-      remoteStream.value.forEach((v) => {
-        const el = createVideo({});
-        el.srcObject = v;
-        videoPlay(el);
+      remoteStreamVideo.value.forEach((videoEl) => {
+        videoPlay(videoEl);
       });
     },
     {
@@ -195,7 +209,6 @@ export function usePull() {
   watch(
     () => cacheStore.muted,
     (newVal) => {
-      appStore.pageIsClick = true;
       rtcVideo.value.forEach((v) => {
         v.muted = newVal;
       });
@@ -241,7 +254,7 @@ export function usePull() {
       roomId: roomId.value,
     });
     const videoEl = createVideo({
-      appendChild: true,
+      appendChild: false,
       muted: appStore.pageIsClick ? cacheStore.muted : true,
     });
     rtcVideo.value.push(videoEl);
@@ -249,8 +262,8 @@ export function usePull() {
       sender: mySocketId.value,
       receiver: 'rtmpToRtc',
       videoEl,
-      sucessCb: (stream) => {
-        remoteStream.value.push(stream);
+      sucessCb: () => {
+        remoteStreamVideo.value.push(videoEl);
       },
     });
     webRtcRtmpToRtc.sendOffer({
@@ -272,7 +285,7 @@ export function usePull() {
       canvasVideoStream: null,
     });
     const videoEl = createVideo({
-      appendChild: true,
+      appendChild: false,
       muted: appStore.pageIsClick ? cacheStore.muted : true,
     });
     rtcVideo.value.push(videoEl);
@@ -280,8 +293,8 @@ export function usePull() {
       sender: mySocketId.value,
       receiver: data.sender,
       videoEl,
-      sucessCb: (stream) => {
-        remoteStream.value.push(stream);
+      sucessCb: () => {
+        remoteStreamVideo.value.push(videoEl);
       },
     });
 
@@ -300,7 +313,7 @@ export function usePull() {
     videoLoading.value = true;
     appStore.liveLine = LiveLineEnum.rtc;
     const videoEl = createVideo({
-      appendChild: true,
+      appendChild: false,
       muted: appStore.pageIsClick ? cacheStore.muted : true,
     });
     rtcVideo.value.push(videoEl);
@@ -310,8 +323,8 @@ export function usePull() {
       sender: mySocketId.value,
       receiver: data.sender,
       videoEl,
-      sucessCb: (stream) => {
-        remoteStream.value.push(stream);
+      sucessCb: () => {
+        remoteStreamVideo.value.push(videoEl);
       },
     });
     webRtcMeetingOne.addTrack({
@@ -413,7 +426,6 @@ export function usePull() {
   }
 
   function handlePlay(data: ILiveRoom) {
-    roomLiving.value = true;
     flvurl.value =
       data.cdn === SwitchEnum.yes ? data.pull_cdn_flv_url! : data.pull_flv_url!;
     hlsurl.value =
@@ -466,9 +478,13 @@ export function usePull() {
             LiveRoomTypeEnum.obs,
             LiveRoomTypeEnum.tencent_css,
             LiveRoomTypeEnum.tencent_css_pk,
+            LiveRoomTypeEnum.forward_all,
             LiveRoomTypeEnum.forward_bilibili,
+            LiveRoomTypeEnum.forward_douyin,
+            LiveRoomTypeEnum.forward_douyu,
             LiveRoomTypeEnum.forward_huya,
-            LiveRoomTypeEnum.forward_all,
+            LiveRoomTypeEnum.forward_kuaishou,
+            LiveRoomTypeEnum.forward_xiaohongshu,
           ].includes(liveRoomInfo.type!)
         ) {
           handlePlay(liveRoomInfo);
@@ -479,6 +495,7 @@ export function usePull() {
         }
       }
       if (!roomLiving.value) {
+        videoLoading.value = false;
         closeRtc();
         handleStopDrawing();
       }
@@ -492,12 +509,10 @@ export function usePull() {
   watch(
     () => appStore.liveLine,
     (newVal) => {
-      console.log('liveLine变了', newVal);
+      console.log('直播线路变了', newVal);
       handleStopDrawing();
-      remoteStream.value = [];
-      if (!roomLiving.value) {
-        return;
-      }
+      remoteStreamVideo.value = [];
+
       switch (newVal) {
         case LiveLineEnum.flv:
           handleFlvPlay();
@@ -566,6 +581,7 @@ export function usePull() {
     initWs,
     initRtcReceive,
     videoWrapRef,
+    stopPlay,
     handlePlay,
     handleStopDrawing,
     initPull,

+ 2 - 2
src/hooks/use-websocket.ts

@@ -16,7 +16,7 @@ import { useWebRtcSrs } from '@/hooks/webrtc/srs';
 import { useWebRtcTencentcloudCss } from '@/hooks/webrtc/tencentcloudCss';
 import {
   DanmuMsgTypeEnum,
-  ILiveUser,
+  ILiveRoomLiveUser,
   IWsMessage,
   SwitchEnum,
   WsMessageContentTypeEnum,
@@ -83,7 +83,7 @@ export const useWebsocket = () => {
   const connectStatus = ref<WsConnectStatusEnum>();
   const loopHeartbeatTimer = ref();
   const loopGetLiveUserTimer = ref();
-  const liveUserList = ref<ILiveUser[]>([]);
+  const liveUserList = ref<ILiveRoomLiveUser[]>([]);
   const roomId = ref('');
   const roomLiving = ref(false);
   const isAnchor = ref(false);

+ 70 - 11
src/interface.ts

@@ -704,6 +704,7 @@ export type ILive = {
   live_room?: ILiveRoom;
   /** 用户信息 */
   user?: IUser;
+  live_record?: ILiveRecord;
 
   created_at?: string;
   updated_at?: string;
@@ -729,7 +730,8 @@ export enum MediaTypeEnum {
   webAudio,
   pk,
   metting,
-  removeGreenVideo,
+  mediaRemoveGreen,
+  cameraRemoveGreen,
 }
 
 export enum DanmuMsgTypeEnum {
@@ -741,16 +743,12 @@ export enum DanmuMsgTypeEnum {
   reward,
 }
 
-export interface ILiveUser {
-  created_at: number;
-  client_ip: string;
-  value: {
-    live_room_id: number;
-    live_room_name: string;
-    user_id: number;
-    user_username: string;
-    user_avatar: string;
-  };
+export interface ILiveRoomLiveUser {
+  live_room_id: number;
+  live_room_name: string;
+  user_id: number;
+  user_username: string;
+  user_avatar: string;
 }
 
 export interface ICredential {
@@ -782,3 +780,64 @@ export interface IPushRes {
   srsPushRes: IStreamKey;
   cdnPushRes: IStreamKey;
 }
+
+export interface ILiveRecord {
+  id?: number;
+  /** 直播平台 */
+  platform?: LivePlatformEnum;
+  /** 直播流名称 */
+  stream_name?: string;
+  /** 直播流id */
+  stream_id?: string;
+  /** 用户id */
+  user_id?: number;
+  /** 直播间id */
+  live_room_id?: number;
+  /** 直播时长(单位:秒) */
+  duration?: number;
+  /** 弹幕数 */
+  danmu?: number;
+  /** 观看数 */
+  view?: number;
+  /** 直播开始时间 */
+  start_time?: string;
+  /** 直播结束时间 */
+  end_time?: string;
+  /** 备注 */
+  remark?: string;
+
+  /** 直播间信息 */
+  live_room?: ILiveRoom;
+  /** 用户信息 */
+  user?: IUser;
+
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: string;
+}
+
+export enum BlacklistTypeEnum {
+  /** 频繁请求 */
+  frequent,
+  /** 管理员禁用 */
+  admin_disable,
+  /** 禁言 */
+  disable_msg,
+}
+
+export interface IBlacklist {
+  id?: number;
+  ip?: string;
+  user_id?: number;
+  type?: BlacklistTypeEnum;
+  start_date?: number;
+  end_date?: number;
+  msg?: string;
+  remark?: string;
+
+  user?: IUser;
+
+  created_at?: string;
+  updated_at?: string;
+  deleted_at?: string;
+}

+ 3 - 1
src/store/user/index.ts

@@ -44,11 +44,13 @@ export const useUserStore = defineStore('user', {
     },
     async usernameLogin({ username, password }) {
       try {
+        const exp = 24 * 7;
         const { data: token } = await fetchUsernameLogin({
           username,
           password,
+          exp,
         });
-        this.setToken(token, 24);
+        this.setToken(token, exp);
         return token;
       } catch (error: any) {
         // 错误返回401,全局的响应拦截会打印报错信息

+ 31 - 6
src/utils/index.ts

@@ -3,6 +3,37 @@ import { createChromakey } from '@webav/av-cliper';
 import { computeBox, getRangeRandom, judgeType } from 'billd-utils';
 import sparkMD5 from 'spark-md5';
 
+/**
+ * 对象排序
+ */
+export function objectSort(data: { obj; sortField; sort?: 'asc' | 'desc' }) {
+  // 将对象转换为数组
+  const entries = Object.entries(data.obj);
+  entries.sort(function (a, b) {
+    // @ts-ignore
+    const res1 = a[1][data.sortField];
+    // @ts-ignore
+    const res2 = b[1][data.sortField];
+    if (data.sort === 'desc') {
+      return res2 - res1;
+    } else {
+      return res1 - res2;
+    }
+  });
+
+  return entries;
+
+  // 将排序后的数组转换回对象
+  // const res: any = {};
+  // for (let i = 0; i < entries.length; i += 1) {
+  //   res[entries[i][0]] = entries[i][1];
+  // }
+  // return res;
+}
+
+/**
+ * 移除视频背景
+ */
 export function videoRemoveBackground(data: {
   videoEl;
   /** 需要扣除的背景色,若不传则取第一个像素点 */
@@ -52,12 +83,6 @@ export function videoRemoveBackground(data: {
           };
           render();
         }
-        // const video = document.createElement('video');
-        // video.srcObject = canvasEl.captureStream();
-        // video.autoplay = true;
-        // video.loop = true;
-        // document.body.appendChild(video);
-        // resolve(video);
         resolve(canvasEl);
       }
     });

+ 0 - 1
src/views/area/index.vue

@@ -61,7 +61,6 @@ watch(
     cursor: pointer;
     &.active {
       color: $theme-color-gold;
-      font-size: 16px;
 
       &::after {
         position: absolute;

+ 0 - 566
src/views/doc/privatizationDeployment/index.vue

@@ -1,566 +0,0 @@
-<template>
-  <div class="privatizationDeployment-wrap">
-    <h2 class="title">
-      <div
-        v-for="(item, index) in detail[currentTab].slogan"
-        :key="index"
-      >
-        {{ item }}
-      </div>
-    </h2>
-    <div class="tab">
-      <div
-        v-for="(item, index) in tab"
-        :key="index"
-        class="item"
-        :class="{ active: item.id === currentTab }"
-        @click="currentTab = item.id"
-      >
-        {{ item.txt }}
-      </div>
-    </div>
-    <div class="list">
-      <div
-        class="item"
-        :class="{ [item['color']]: 1 }"
-        v-for="(item, index) in detail[currentTab].list"
-        :key="index"
-      >
-        <div class="name">{{ item.name }}</div>
-        <div class="desc">{{ item.short_desc }}</div>
-        <div class="price">
-          <span class="t1">{{ item.price.left }}</span>
-          <span class="t2">{{ item.price.center }}</span>
-          <span class="t3">{{ item.price.right }}</span>
-        </div>
-        <div class="feat">
-          <div
-            v-if="item.tip !== ''"
-            class="feat-item tip"
-          >
-            {{ item.tip }}
-          </div>
-          <div
-            class="feat-item"
-            v-for="(iten, indey) in item.feat"
-            :key="indey"
-          >
-            <div
-              :class="{
-                done: iten.status === 'done',
-                todo: iten.status === 'todo',
-              }"
-            ></div>
-            <div class="txt">{{ iten.txt }}</div>
-          </div>
-        </div>
-        <div
-          class="btn"
-          @click="handleClick(item.btn)"
-        >
-          {{ item.btn.txt }}
-        </div>
-      </div>
-    </div>
-  </div>
-  <n-modal v-model:show="showContach">
-    <n-card
-      style="width: 400px"
-      title="联系方式"
-      role="dialog"
-      closable
-      @close="showContach = false"
-    >
-      <div>
-        <div>微信:</div>
-        <img
-          src="@/assets/img/my-wechat.webp"
-          alt=""
-          style="width: 300px"
-        />
-        <div>微信号: {{ AUTHOR_INFO.wechat }}</div>
-        <div>qq:{{ AUTHOR_INFO.qq }}</div>
-        <p>添加时请备注:私有化部署+用途</p>
-      </div>
-    </n-card>
-  </n-modal>
-</template>
-
-<script lang="ts" setup>
-import { openToTarget } from 'billd-utils';
-import { ref } from 'vue';
-import { useRouter } from 'vue-router';
-
-import { AUTHOR_INFO, COMMON_URL, PROJECT_NAME } from '@/constant';
-import { routerName } from '@/router';
-
-const router = useRouter();
-const showContach = ref(false);
-const currentTab = ref<'personal' | 'openSource' | 'customized' | string>(
-  'openSource'
-);
-
-const tab = ref([
-  {
-    id: 'personal',
-    txt: '个人版',
-  },
-  {
-    id: 'openSource',
-    txt: '开源版',
-  },
-  {
-    id: 'customized',
-    txt: '定制版',
-  },
-]);
-
-const detail = ref({
-  personal: {
-    slogan: ['欢迎使用billd直播~'],
-    list: [
-      {
-        color: 'blue',
-        name: 'VIP',
-        desc: '适用于个人用户简单体验',
-        price: {
-          left: '¥',
-          center: '0',
-          right: '',
-        },
-        tip: '',
-        feat: [
-          {
-            status: 'done',
-            txt: 'SRS直播',
-          },
-          {
-            status: 'done',
-            txt: '打PK直播',
-          },
-          {
-            status: 'done',
-            txt: 'WebRTC直播',
-          },
-          {
-            status: 'done',
-            txt: 'WebRTC会议',
-          },
-        ],
-        btn: {
-          type: 'push',
-          link: routerName.push,
-          txt: '免费体验',
-        },
-      },
-      {
-        color: 'green',
-        name: 'SVIP',
-        desc: '适用于个人用户中度体验',
-        price: {
-          left: '¥',
-          center: '10',
-          right: '元/月',
-        },
-        tip: '涵盖VIP全部功能',
-        feat: [
-          {
-            status: 'done',
-            txt: '转推b站',
-          },
-          {
-            status: 'done',
-            txt: '转推虎牙',
-          },
-          {
-            status: 'done',
-            txt: '去广告',
-          },
-        ],
-        btn: {
-          type: 'toast',
-          link: '即将上线~',
-          txt: '立即购买',
-        },
-      },
-      {
-        color: 'orange',
-        name: 'ADMIN',
-        desc: '适用于个人用户深度体验',
-        price: {
-          left: '¥',
-          center: '50',
-          right: '元/月',
-        },
-        tip: '涵盖SVIP全部功能',
-        feat: [
-          {
-            status: 'done',
-            txt: 'Msr直播',
-          },
-          {
-            status: 'done',
-            txt: '腾讯云直播(CDN)',
-          },
-          {
-            status: 'done',
-            txt: '腾讯云打PK(CDN)',
-          },
-        ],
-        btn: {
-          type: 'toast',
-          link: '即将上线~',
-          txt: '立即购买',
-        },
-      },
-    ],
-  },
-  openSource: {
-    slogan: ['billd直播开源至今,累计收获1.3k+ star', '值得信赖,欢迎部署~'],
-    list: [
-      {
-        color: 'blue',
-        name: 'Github',
-        desc: '适用于个人学习/测试用途',
-        price: {
-          left: '¥',
-          center: '0',
-          right: '',
-        },
-        tip: '',
-        feat: [
-          {
-            status: 'done',
-            txt: '源码开源,自行部署',
-          },
-          {
-            status: 'done',
-            txt: '直播前台(Web)',
-          },
-          {
-            status: 'done',
-            txt: '直播后台(Web)',
-          },
-          {
-            status: 'done',
-            txt: '直播安卓端(Flutter)',
-          },
-          {
-            status: 'todo',
-            txt: '直播苹果端(Flutter)',
-          },
-          {
-            status: 'todo',
-            txt: '直播客户端(Electron)',
-          },
-        ],
-        btn: {
-          type: 'link',
-          link: 'https://github.com/galaxy-s10/billd-live',
-          txt: '立即部署',
-        },
-      },
-      {
-        color: 'green',
-        name: '私有化部署',
-        desc: '适用于个人/企业自建直播平台',
-        price: {
-          left: '¥',
-          center: 'XXXX',
-          right: '起',
-        },
-        tip: '涵盖Github全部/部分功能',
-        feat: [
-          {
-            status: 'done',
-            txt: '无门槛,全程专人负责部署',
-          },
-          {
-            status: 'done',
-            txt: '本地服务器部署',
-          },
-          {
-            status: 'done',
-            txt: '快速上线',
-          },
-        ],
-        btn: {
-          type: 'showContact',
-          link: '',
-          txt: '立即咨询',
-        },
-      },
-    ],
-  },
-  customized: {
-    slogan: ['billd直播支持定制化', '立即定制自己的个性化直播间~'],
-    list: [
-      {
-        color: 'blue',
-        name: '在线咨询',
-        desc: '咨询/答疑服务',
-        price: {
-          left: '¥',
-          center: '50',
-          right: '元/小时',
-        },
-        tip: '',
-        feat: [
-          {
-            status: 'done',
-            txt: '一对一解答',
-          },
-        ],
-        btn: {
-          type: 'showContact',
-          link: '',
-          txt: '立即咨询',
-        },
-      },
-      {
-        color: 'green',
-        name: '付费课程',
-        desc: '适用于前端/音视频小白',
-        price: {
-          left: '¥',
-          center: '399',
-          right: '元',
-        },
-        tip: '',
-        feat: [
-          {
-            status: 'done',
-            txt: '一对一解答(4小时)',
-          },
-          {
-            status: 'done',
-            txt: '能够搭建最基础的直播间',
-          },
-          {
-            status: 'done',
-            txt: '单独的代码仓库',
-          },
-          {
-            status: 'done',
-            txt: '视频讲解',
-          },
-          {
-            status: 'done',
-            txt: `${PROJECT_NAME}付费课微信群`,
-          },
-        ],
-        btn: {
-          type: 'link',
-          link: COMMON_URL.payCoursesArticle,
-          txt: '了解详情',
-        },
-      },
-      {
-        color: 'orange',
-        name: '私有化部署',
-        desc: '适用于个人/企业自建直播平台',
-        price: {
-          left: '¥',
-          center: 'XXXX',
-          right: '起',
-        },
-        tip: '',
-        feat: [
-          {
-            status: 'done',
-            txt: '无门槛,全程专人负责部署',
-          },
-          {
-            status: 'done',
-            txt: '本地服务器部署',
-          },
-          {
-            status: 'done',
-            txt: '快速上线',
-          },
-          {
-            status: 'done',
-            txt: '定制化功能',
-          },
-        ],
-        btn: {
-          type: 'showContact',
-          link: '',
-          txt: '立即咨询',
-        },
-      },
-    ],
-  },
-});
-
-function handleClick(item) {
-  if (item.type === 'link') {
-    openToTarget(item.link);
-  } else if (item.type === 'push') {
-    const url = router.resolve({
-      name: item.link,
-    });
-    openToTarget(url.href);
-  } else if (item.type === 'buy') {
-    console.log('buy');
-  } else if (item.type === 'toast') {
-    window.$message.info(item.link);
-  } else if (item.type === 'showContact') {
-    showContach.value = true;
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.privatizationDeployment-wrap {
-  height: calc(100vh - $layout-head-h);
-  background-color: #f4f8ff;
-  .title {
-    display: flex;
-    flex-direction: column;
-    justify-content: center;
-    box-sizing: border-box;
-    margin: 0 auto;
-    width: 1200px;
-    height: 180px;
-    // background-color: red;
-    text-align: center;
-    font-size: 40px;
-  }
-  .tab {
-    display: flex;
-    justify-content: center;
-    margin: 0 auto;
-    padding: 8px 0;
-    width: 320px;
-    border-radius: 40px;
-    background: white;
-    box-shadow: 0 3px 6px rgba(0, 0, 0, 0.05);
-
-    user-select: none;
-    .item {
-      padding: 4px 25px;
-      border-radius: 40px;
-      color: #686e88;
-      font-weight: 700;
-      font-size: 16px;
-      cursor: pointer;
-      &.active {
-        background-color: $theme-color-gold;
-        color: white;
-      }
-    }
-  }
-  .list {
-    display: flex;
-    justify-content: center;
-    margin: 50px auto 0;
-    width: 1200px;
-    .item {
-      box-sizing: border-box;
-      margin: 0 10px;
-      padding: 20px 20px;
-      width: 240px;
-      // border: 1px solid #dde6ed;
-      border-radius: 2px;
-      border-top-left-radius: 4px;
-      border-top-right-radius: 4px;
-      background-color: white;
-      font-size: 14px;
-
-      &.blue {
-        border-top: 7px solid #38c0ff;
-      }
-      &.green {
-        border-top: 7px solid #30d1aa;
-      }
-      &.orange {
-        border-top: 7px solid #ffbd33;
-      }
-      .name {
-        padding: 10px 0 0;
-        height: 40px;
-        text-align: center;
-        font-size: 30px;
-        line-height: 1;
-      }
-      .desc {
-        margin-top: 10px;
-        height: 40px;
-        color: #88898d;
-        text-align: center;
-        // background-color: red;
-      }
-      .price {
-        display: flex;
-        align-items: flex-end;
-        justify-content: center;
-        height: 45px;
-        color: #88898d;
-        text-align: center;
-        .t1 {
-          color: #272727;
-          font-weight: 600;
-          font-size: 16px;
-        }
-        .t2 {
-          color: #272727;
-          font-size: 40px;
-          line-height: 36px;
-        }
-        .t3 {
-          color: #2c2c2c;
-          font-size: 16px;
-        }
-        // background-color: red;
-      }
-      .feat {
-        margin-top: 30px;
-        height: 200px;
-        .feat-item {
-          display: flex;
-          align-items: center;
-          margin-bottom: 10px;
-          &.tip {
-            color: #88898d;
-          }
-          .todo,
-          .done {
-            margin-right: 10px;
-            width: 18px;
-            height: 18px;
-            text-align: center;
-          }
-          .todo {
-            position: relative;
-            &::after {
-              color: #ffc049;
-              content: '-';
-              text-align: center;
-              font-size: 16px;
-            }
-          }
-          .done {
-            @include setBackground('@/assets/img/check.png');
-          }
-        }
-      }
-      .btn {
-        margin: 0 auto;
-        padding: 8px 0;
-        width: 160px;
-        border: 1px solid $theme-color-gold;
-        border-radius: 4px;
-        color: $theme-color-gold;
-        text-align: center;
-        font-size: 16px;
-        cursor: pointer;
-        &:hover {
-          background-color: $theme-color-gold;
-          color: white;
-        }
-      }
-    }
-  }
-}
-</style>

+ 66 - 12
src/views/h5/area/index.vue

@@ -18,7 +18,9 @@
         >
           <div
             class="cover"
-            v-lazy:background-image="iten?.cover_img || iten?.users?.[0].avatar"
+            v-lazy:background-image="
+              iten?.cover_img || iten?.users?.[0]?.avatar
+            "
           >
             <div
               v-if="iten?.live"
@@ -27,13 +29,7 @@
               <div class="live-txt">直播中</div>
             </div>
             <div
-              v-if="
-                iten?.cdn === SwitchEnum.yes ||
-                [
-                  LiveRoomTypeEnum.tencent_css,
-                  LiveRoomTypeEnum.tencent_css_pk,
-                ].includes(iten.type!)
-              "
+              v-if="iten?.cdn === SwitchEnum.yes"
               class="cdn-ico"
             >
               <div class="txt">CDN</div>
@@ -52,10 +48,12 @@ import { onMounted, reactive, ref } from 'vue';
 import { useRoute } from 'vue-router';
 
 import { fetchLiveRoomList } from '@/api/area';
+import { fetchLiveBilibiliGetUserRecommend } from '@/api/bilibili';
 import LongList from '@/components/LongList/index.vue';
+import { URL_QUERY } from '@/constant';
 import { SwitchEnum } from '@/interface';
 import router, { routerName } from '@/router';
-import { ILiveRoom, LiveRoomTypeEnum } from '@/types/ILiveRoom';
+import { ILiveRoom } from '@/types/ILiveRoom';
 
 const route = useRoute();
 
@@ -64,7 +62,7 @@ const topRef = ref<HTMLDivElement>();
 
 const pageParams = reactive({
   nowPage: 0,
-  pageSize: 50,
+  pageSize: 20,
 });
 const height = ref(0);
 const loading = ref(false);
@@ -73,6 +71,15 @@ const longListRef = ref<InstanceType<typeof LongList>>();
 const status = ref<'loading' | 'nonedata' | 'allLoaded' | 'normal'>('loading');
 
 function goRoom(item: ILiveRoom) {
+  // @ts-ignore
+  if (item.is_bilibili) {
+    router.push({
+      name: routerName.h5Room,
+      params: { roomId: item.id },
+      query: { [URL_QUERY.isBilibili]: 'true' },
+    });
+    return;
+  }
   if (!item.live) {
     window.$message.info('该直播间没在直播~');
     return;
@@ -83,13 +90,55 @@ function goRoom(item: ILiveRoom) {
   });
 }
 
+async function handleBilibilData() {
+  try {
+    if (loading.value) return;
+    loading.value = true;
+    pageParams.nowPage += 1;
+    status.value = 'loading';
+    pageParams.nowPage += 1;
+    const res = await fetchLiveBilibiliGetUserRecommend({
+      page: pageParams.nowPage,
+      page_size: pageParams.pageSize,
+      platform: 'web',
+    });
+    const list = res?.data?.data?.list;
+    if (list) {
+      const arr = list.map((item) => {
+        return {
+          id: item.roomid,
+          name: item.title,
+          cover_img: item.cover,
+          users: [{ username: item.uname }],
+          is_bilibili: true,
+          live: {},
+          cdn: SwitchEnum.yes,
+        };
+      });
+      hasMore.value = res.data.data.has_more === 1 ? true : false;
+      liveRoomList.value.push(...arr);
+    }
+  } catch (error) {
+    pageParams.nowPage -= 1;
+    console.log(error);
+  }
+  loading.value = false;
+  status.value = 'normal';
+  status.value = 'normal';
+  handleStatus();
+}
+
 onMounted(() => {
   if (topRef.value) {
     height.value =
       document.documentElement.clientHeight -
       topRef.value.getBoundingClientRect().top;
   }
-  getData();
+  if (route.query[URL_QUERY.isBilibili] === 'true') {
+    handleBilibilData();
+  } else {
+    getData();
+  }
 });
 
 function handleStatus() {
@@ -129,9 +178,14 @@ async function getData() {
   status.value = 'normal';
   handleStatus();
 }
+
 function getListData() {
   if (!hasMore.value) return;
-  getData();
+  if (route.query[URL_QUERY.isBilibili] === 'true') {
+    handleBilibilData();
+  } else {
+    getData();
+  }
 }
 </script>
 

+ 87 - 3
src/views/h5/index.vue

@@ -69,6 +69,42 @@
           </div>
         </div>
       </div>
+      <div class="item">
+        <div
+          class="title"
+          @click.stop
+        >
+          <div class="left">b站直播</div>
+          <div
+            class="right"
+            @click="showAllBilibili()"
+          >
+            查看全部
+          </div>
+        </div>
+        <div class="live-room-list">
+          <div
+            v-for="(item, indey) in bilibiliLiveRoomList"
+            :key="indey"
+            class="live-room"
+            @click="goRoom(item, true)"
+          >
+            <div
+              class="cover"
+              v-lazy:background-image="
+                item.live_room?.cover_img || item?.user?.avatar
+              "
+            >
+              <div class="living-ico">直播中</div>
+              <div class="cdn-ico">
+                <div class="txt">CDN</div>
+              </div>
+              <div class="txt">{{ item?.live_room?.users?.[0].username }}</div>
+            </div>
+            <div class="desc">{{ item?.live_room?.name }}</div>
+          </div>
+        </div>
+      </div>
     </div>
   </div>
 </template>
@@ -79,8 +115,9 @@ import { onMounted, onUnmounted, ref } from 'vue';
 import { useI18n } from 'vue-i18n';
 
 import { fetchAreaLiveRoomList } from '@/api/area';
-import { COMMON_URL } from '@/constant';
-import { IArea, SwitchEnum } from '@/interface';
+import { fetchLiveBilibiliGetUserRecommend } from '@/api/bilibili';
+import { COMMON_URL, URL_QUERY } from '@/constant';
+import { IArea, ILive, SwitchEnum } from '@/interface';
 import router, { mobileRouterName, routerName } from '@/router';
 import { useAppStore } from '@/store/app';
 import {
@@ -109,6 +146,7 @@ const swiperList = ref([
 ]);
 const swiperTimer = ref();
 const currentSwiper = ref(swiperList.value[0]);
+const bilibiliLiveRoomList = ref<ILive[]>([]);
 
 async function getLiveRoomList() {
   try {
@@ -143,7 +181,23 @@ function showAll(item: IArea) {
   });
 }
 
-function goRoom(item: ILiveRoom) {
+function showAllBilibili() {
+  router.push({
+    name: mobileRouterName.h5Area,
+    params: { id: 1 },
+    query: { [URL_QUERY.isBilibili]: 'true' },
+  });
+}
+
+function goRoom(item: ILiveRoom, isBilibili = false) {
+  if (isBilibili) {
+    router.push({
+      name: routerName.h5Room,
+      params: { roomId: item.id },
+      query: { [URL_QUERY.isBilibili]: 'true' },
+    });
+    return;
+  }
   if (!item.live) {
     window.$message.info('该直播间没在直播~');
     return;
@@ -164,12 +218,42 @@ onMounted(() => {
     }
     currentSwiper.value = swiperList.value[num];
   }, 3000);
+  handleBilibilData();
 });
 
 onUnmounted(() => {
   clearInterval(swiperTimer.value);
   appStore.showLoginModal = false;
 });
+
+async function handleBilibilData() {
+  let arr: any = [];
+  try {
+    const res = await fetchLiveBilibiliGetUserRecommend({
+      page: 1,
+      page_size: 4,
+      platform: 'web',
+    });
+    // const list = res.data?.list;
+    const list = res?.data?.data?.list;
+    if (list) {
+      arr = list.map((item) => {
+        return {
+          id: item.roomid,
+          user: { username: item.uname },
+          live_room: {
+            id: item.roomid,
+            name: item.title,
+            cover_img: item.cover,
+          },
+        };
+      });
+    }
+  } catch (error) {
+    console.log(error);
+  }
+  bilibiliLiveRoomList.value = arr;
+}
 </script>
 
 <style lang="scss" scoped>

+ 54 - 12
src/views/h5/room/index.vue

@@ -50,7 +50,7 @@
         点击播放
       </div>
       <VideoControls
-        v-if="roomLiving"
+        v-if="roomLiving && liveRoomInfo"
         :resolution="videoResolution"
         @refresh="handleRefresh"
         @full-screen="handleFullScreen"
@@ -169,12 +169,12 @@
             >
               <div class="info">
                 <Avatar
-                  :url="item.value.user_avatar"
-                  :name="item.value.user_username"
+                  :url="item.user_avatar"
+                  :name="item.user_username"
                   :size="25"
                 ></Avatar>
                 <div class="username">
-                  {{ item.value.user_username }}
+                  {{ item.user_username }}
                 </div>
               </div>
             </div>
@@ -271,10 +271,14 @@ import { windowReload } from 'billd-utils';
 import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
 import { useRoute } from 'vue-router';
 
+import {
+  fetchLiveBilibiliPlayUrl,
+  fetchLiveBilibiliRoomGetInfo,
+} from '@/api/bilibili';
 import { fetchLiveRoomOnlineUser } from '@/api/live';
 import { fetchFindLiveRoom } from '@/api/liveRoom';
 import { fetchGetWsMessageList } from '@/api/wsMessage';
-import { THEME_COLOR } from '@/constant';
+import { THEME_COLOR, URL_QUERY } from '@/constant';
 import { emojiArray } from '@/emoji';
 import { useFullScreen, usePictureInPicture } from '@/hooks/use-play';
 import { usePull } from '@/hooks/use-pull';
@@ -289,7 +293,7 @@ import router, { mobileRouterName } from '@/router';
 import { useAppStore } from '@/store/app';
 import { useCacheStore } from '@/store/cache';
 import { useUserStore } from '@/store/user';
-import { ILiveRoom } from '@/types/ILiveRoom';
+import { ILiveRoom, LiveRoomTypeEnum } from '@/types/ILiveRoom';
 import { IUser } from '@/types/IUser';
 import { formatTimeHour } from '@/utils';
 
@@ -302,6 +306,7 @@ const bottomRef = ref<HTMLDivElement>();
 const danmuListRef = ref<HTMLDivElement>();
 const showEmoji = ref(false);
 const showPlayBtn = ref(true);
+const isBilibili = ref(false);
 
 const liveRoomInfo = ref<ILiveRoom>();
 const anchorInfo = ref<IUser>();
@@ -341,12 +346,7 @@ onMounted(async () => {
   setTimeout(() => {
     scrollTo(0, 0);
   }, 100);
-
   roomId.value = route.params.roomId as string;
-  initPull({ roomId: roomId.value, autolay: true });
-  await handleFindLiveRoomInfo();
-  if (!liveRoomInfo.value) return;
-  handleRefresh();
   videoWrapRef.value = remoteVideoRef.value;
   videoWrapHeight.value =
     document.documentElement.clientWidth / appStore.videoRatio;
@@ -358,6 +358,17 @@ onMounted(async () => {
       containerHeight.value = res;
     }
   });
+  if (route.query[URL_QUERY.isBilibili] === 'true') {
+    isBilibili.value = true;
+    initWs({ roomId: roomId.value, isBilibili: true, isAnchor: false });
+    handleBilibil();
+    return;
+  }
+  initPull({ roomId: roomId.value, autolay: true });
+  await handleFindLiveRoomInfo();
+  if (!liveRoomInfo.value) return;
+  handleRefresh();
+
   await handleFindLiveRoomInfo();
   handleSendGetLiveUser(Number(roomId.value));
   handleHistoryMsg();
@@ -369,6 +380,37 @@ onMounted(async () => {
   initRtcReceive();
 });
 
+async function handleBilibil() {
+  const flv = await fetchLiveBilibiliPlayUrl({
+    cid: route.params.roomId,
+    platform: 'web',
+  });
+  const hls = await fetchLiveBilibiliPlayUrl({
+    cid: route.params.roomId,
+    platform: 'h5',
+  });
+  const roomInfo = await fetchLiveBilibiliRoomGetInfo({
+    room_id: route.params.roomId,
+  });
+  console.log(flv?.data?.data?.durl?.[0].url, 'flv');
+  console.log(hls?.data?.data?.durl?.[0].url, 'hls');
+  roomLiving.value = true;
+  anchorInfo.value = {
+    avatar: roomInfo?.data?.data?.user_cover,
+    username: roomInfo?.data?.data?.title,
+  };
+  liveRoomInfo.value = {
+    type: LiveRoomTypeEnum.system,
+    pull_flv_url: flv?.data?.data?.durl?.[0].url,
+    pull_hls_url: hls?.data?.data?.durl?.[0].url,
+    areas: [{ name: roomInfo?.data?.data?.area_name }],
+    desc: roomInfo?.data?.data?.description,
+    name: roomInfo?.data?.data?.title,
+    cover_img: roomInfo?.data?.data?.user_cover,
+  };
+  handleRefresh();
+}
+
 watch(
   () => roomLiving.value,
   (newval) => {
@@ -427,7 +469,7 @@ async function handleHistoryMsg() {
 function handleSendGetLiveUser(liveRoomId: number) {
   clearInterval(loopGetLiveUserTimer.value);
   async function main() {
-    const res = await fetchLiveRoomOnlineUser({ live_room_id: liveRoomId });
+    const res = await fetchLiveRoomOnlineUser(liveRoomId);
     if (res.code === 200) {
       liveUserList.value = res.data;
     }

+ 15 - 31
src/views/home/index.vue

@@ -19,22 +19,6 @@
         autoplay
         loop
       ></video>
-      <!-- <div class="slider-wrap">
-        <div
-          v-for="(item, index) in interactionList"
-          :key="index"
-        >
-          <Slider
-            v-if="item.length"
-            :list="item"
-            :width="docW"
-            :speed="60"
-            :direction="index % 2 === 0 ? 'l-r' : 'r-l'"
-            :customStyle="{ margin: '0 auto' }"
-          ></Slider>
-        </div>
-      </div> -->
-
       <div class="container">
         <div
           v-loading="videoLoading"
@@ -82,7 +66,7 @@
             >
               <div
                 class="btn"
-                @click="joinRoom(currentLive?.live_room, 2)"
+                @click="joinRoom(currentLive?.live_room, 'false')"
               >
                 进入直播
               </div>
@@ -147,7 +131,7 @@
             v-for="(item, indey) in otherLiveRoomList"
             :key="indey"
             class="live-room"
-            @click="joinRoom(item.live_room, 2)"
+            @click="joinRoom(item.live_room, 'false')"
           >
             <div
               class="cover"
@@ -181,7 +165,7 @@
             v-for="(iten, indey) in bilibiliLiveRoomList"
             :key="indey"
             class="live-room"
-            @click="joinRoom(iten.live_room, 1)"
+            @click="joinRoom(iten.live_room, 'true')"
           >
             <div
               class="cover"
@@ -281,6 +265,7 @@ const {
   videoResolution,
   handleStopDrawing,
   handlePlay,
+  stopPlay,
 } = usePull();
 const isBottom = ref(false);
 const rootMargin = {
@@ -411,11 +396,13 @@ async function getLiveRoomList() {
   }
 }
 
-function joinRoom(data, isBilibili) {
+function joinRoom(data, isBilibili = 'false') {
+  stopPlay();
+  const query = isBilibili === 'true' ? { [URL_QUERY.isBilibili]: 'true' } : {};
   const url = router.resolve({
     name: routerName.pull,
     params: { roomId: data.id },
-    query: { [URL_QUERY.isBilibili]: isBilibili },
+    query,
   });
   openToTarget(url.href);
 }
@@ -423,12 +410,10 @@ function joinRoom(data, isBilibili) {
 
 <style lang="scss" scoped>
 .home-wrap {
-  // overflow: scroll;
-  // height: 100vh;
-  // height: calc(100vh - $header-height);
   .play-container {
     position: relative;
     z-index: 1;
+    padding-top: 20px;
     padding-bottom: 50px;
     .bg-img {
       position: absolute;
@@ -459,8 +444,7 @@ function joinRoom(data, isBilibili) {
       justify-content: center;
       box-sizing: border-box;
       margin: 0 auto;
-      padding-top: 20px;
-      height: calc($w-1100 / $video-ratio);
+      height: calc($w-1150 / $video-ratio);
 
       .left {
         position: relative;
@@ -469,7 +453,7 @@ function joinRoom(data, isBilibili) {
         flex-shrink: 0;
         box-sizing: border-box;
         margin-right: 20px;
-        width: $w-1100;
+        width: $w-1150;
         height: 100%;
         border-radius: 4px;
         background-color: rgba($color: #000000, $alpha: 0.3);
@@ -519,8 +503,8 @@ function joinRoom(data, isBilibili) {
           left: 50%;
           // min-width: 100%;
           // min-height: 100%;
-          max-width: $w-1100;
-          max-height: calc($w-1100 / $video-ratio);
+          max-width: $w-1150;
+          max-height: calc($w-1150 / $video-ratio);
           transform: translate(-50%, -50%);
 
           user-select: none;
@@ -531,8 +515,8 @@ function joinRoom(data, isBilibili) {
           left: 50%;
           // min-width: 100%;
           // min-height: 100%;
-          max-width: $w-1100;
-          max-height: calc($w-1100 / $video-ratio);
+          max-width: $w-1150;
+          max-height: calc($w-1150 / $video-ratio);
           transform: translate(-50%, -50%);
 
           user-select: none;

+ 141 - 48
src/views/pull/index.vue

@@ -35,7 +35,7 @@
           <Avatar
             :url="anchorInfo?.avatar"
             :name="anchorInfo?.username"
-            :size="55"
+            :size="60"
             @click="
               router.push({
                 name: routerName.my,
@@ -96,9 +96,21 @@
         </div>
         <div class="other">
           <div class="top">
-            <div class="item">666人看过</div>
-            <div class="item">666点赞</div>
-            <div class="item">当前在线:{{ liveUserList.length }}人</div>
+            <div class="item">
+              <i class="ico browse"></i>
+              {{ liveRoomInfo.live?.live_record?.view }}人看过
+            </div>
+            <div class="item">
+              <i class="ico good"></i>
+              {{ liveRoomInfo.live?.live_record?.danmu }} 点赞
+            </div>
+            <div
+              class="item hover"
+              @click="handleShare"
+            >
+              <i class="ico share"></i>
+              分享
+            </div>
           </div>
           <div class="bottom">
             <n-popover
@@ -223,7 +235,7 @@
     <div class="right">
       <div class="rank-wrap">
         <div class="tab">
-          <span>在线用户</span>
+          <span>在线用户({{ liveUserList.length }})</span>
           <span> | </span>
           <span>排行榜</span>
         </div>
@@ -235,12 +247,12 @@
           >
             <div class="info">
               <Avatar
-                :url="item.value.user_avatar"
-                :name="item.value.user_username"
+                :url="item.user_avatar"
+                :name="item.user_username"
                 :size="25"
               ></Avatar>
               <div class="username">
-                {{ item.value.user_username }}
+                {{ item.user_username }}
               </div>
             </div>
           </div>
@@ -263,34 +275,53 @@
             </div>
           </template>
           <template v-if="item.msg_type === DanmuMsgTypeEnum.danmu">
+            <span class="time">
+              [{{ formatTimeHour(item.send_msg_time!) }}]
+            </span>
             <span class="name">
               <Dropdown
                 trigger="hover"
                 positon="left"
               >
                 <template #btn>
-                  <span class="time">
-                    [{{ formatTimeHour(item.send_msg_time!) }}]
-                  </span>
                   <span class="username">{{ item.username }}</span>
                   <span class="role">
                     [{{ item.user?.roles?.map((v) => v.role_name).join() }}]
                   </span>
                 </template>
-                <template #list>
+                <template
+                  #list
+                  v-if="
+                    userStore.userInfo?.roles?.find(
+                      (v) => v.id === DEFAULT_ROLE_INFO.SUPER_ADMIN.id
+                    )
+                  "
+                >
                   <div class="list">
                     <div class="item">{{ item.username }}</div>
                     <div
                       class="item operator"
-                      @click="handleDisableSpeakingUser()"
+                      @click="handleBlacklistAddDisableMsg(item.user?.id)"
+                    >
+                      禁言该用户
+                    </div>
+                    <div
+                      class="item operator"
+                      @click="handleBlacklistDelDisableMsg(item.user?.id)"
+                    >
+                      解除禁言该用户
+                    </div>
+                    <div
+                      class="item operator"
+                      @click="handleBlacklistAddAdminDisable(item.user?.id)"
                     >
-                      禁言
+                      禁用该用户
                     </div>
                     <div
                       class="item operator"
-                      @click="handleCancelDisableSpeakingUser()"
+                      @click="handleBlacklistDelAdminDisable(item.user?.id)"
                     >
-                      解除禁言
+                      解除禁用该用户
                     </div>
                   </div>
                 </template>
@@ -409,6 +440,7 @@
 </template>
 
 <script lang="ts" setup>
+import { copyToClipBoard } from 'billd-utils';
 import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useRoute } from 'vue-router';
@@ -417,6 +449,12 @@ import {
   fetchLiveBilibiliPlayUrl,
   fetchLiveBilibiliRoomGetInfo,
 } from '@/api/bilibili';
+import {
+  fetchBlacklistAddAdminDisable,
+  fetchBlacklistAddDisableMsg,
+  fetchBlacklistDelAdminDisable,
+  fetchBlacklistDelDisableMsg,
+} from '@/api/blacklist';
 import {
   fetchGiftGroupList,
   fetchGiftRecordCreate,
@@ -426,11 +464,12 @@ import { fetchGoodsList } from '@/api/goods';
 import { fetchLiveRoomOnlineUser } from '@/api/live';
 import { fetchFindLiveRoom, fetchLiveRoomBilibili } from '@/api/liveRoom';
 import { fetchGetWsMessageList } from '@/api/wsMessage';
-import { liveRoomTypeEnumMap, URL_QUERY } from '@/constant';
+import { DEFAULT_ROLE_INFO, liveRoomTypeEnumMap, URL_QUERY } from '@/constant';
 import { emojiArray } from '@/emoji';
 import { commentAuthTip, loginTip } from '@/hooks/use-login';
 import { useFullScreen, usePictureInPicture } from '@/hooks/use-play';
 import { usePull } from '@/hooks/use-pull';
+import { useTip } from '@/hooks/use-tip';
 import { useUpload } from '@/hooks/use-upload';
 import { useWebsocket } from '@/hooks/use-websocket';
 import {
@@ -439,7 +478,6 @@ import {
   GoodsTypeEnum,
   IGiftRecord,
   IGoods,
-  LiveLineEnum,
   LiveRenderEnum,
   WsMessageContentTypeEnum,
   WsMessageIsFileEnum,
@@ -453,7 +491,7 @@ import { useNetworkStore } from '@/store/network';
 import { useUserStore } from '@/store/user';
 import { ILiveRoom, LiveRoomTypeEnum } from '@/types/ILiveRoom';
 import { IUser } from '@/types/IUser';
-import { formatMoney, formatTimeHour } from '@/utils';
+import { formatMoney, formatTimeHour, getLiveRoomPageUrl } from '@/utils';
 import { NODE_ENV } from 'script/constant';
 
 import RechargeCpt from './recharge/index.vue';
@@ -552,7 +590,7 @@ onMounted(async () => {
   }, 100);
   roomId.value = route.params.roomId as string;
   initPull({ roomId: roomId.value, autolay: true });
-  if (route.query[URL_QUERY.isBilibili] === '1') {
+  if (route.query[URL_QUERY.isBilibili] === 'true') {
     isBilibili.value = true;
     const res = await fetchLiveRoomBilibili();
     roomId.value = `${res.data.id!}`;
@@ -560,7 +598,9 @@ onMounted(async () => {
   initRoomId(roomId.value);
   await handleFindLiveRoomInfo();
   if (!liveRoomInfo.value) return;
-  handleRefresh();
+  if (liveRoomInfo.value.live) {
+    handleRefresh();
+  }
   appStore.videoControls.fps = true;
   appStore.videoControls.fullMode = true;
   appStore.videoControls.kbs = true;
@@ -604,6 +644,21 @@ onUnmounted(() => {
   clearInterval(loopGetLiveUserTimer.value);
 });
 
+function handleShare() {
+  useTip({
+    content: `直播间地址:${getLiveRoomPageUrl(+roomId.value)}`,
+    title: '分享',
+    confirmButtonText: '复制',
+    hiddenCancel: true,
+    maskClosable: true,
+  })
+    .then(() => {
+      copyToClipBoard(getLiveRoomPageUrl(+roomId.value));
+      window.$message.success('复制成功');
+    })
+    .catch();
+}
+
 async function handleFindLiveRoomInfo() {
   try {
     const res = await fetchFindLiveRoom(Number(roomId.value));
@@ -641,7 +696,6 @@ async function handleBilibil() {
   console.log(flv?.data?.data?.durl?.[0].url, 'flv');
   console.log(hls?.data?.data?.durl?.[0].url, 'hls');
   roomLiving.value = true;
-  appStore.liveLine = appStore.mseSupport ? LiveLineEnum.flv : LiveLineEnum.hls;
   anchorInfo.value = {
     avatar: roomInfo?.data?.data?.user_cover,
     username: roomInfo?.data?.data?.title,
@@ -659,7 +713,7 @@ async function handleBilibil() {
 function handleSendGetLiveUser(liveRoomId: number) {
   clearInterval(loopGetLiveUserTimer.value);
   async function main() {
-    const res = await fetchLiveRoomOnlineUser({ live_room_id: liveRoomId });
+    const res = await fetchLiveRoomOnlineUser(liveRoomId);
     if (res.code === 200) {
       liveUserList.value = res.data;
     }
@@ -762,17 +816,32 @@ watch(
   }
 );
 
-/**
- * 禁言用户逻辑:
- * 主播开播了,可以禁言所有看自己直播的用户
- * 使用redis存储记录,key是主播直播间id,value是禁言用户id
- */
-function handleDisableSpeakingUser() {
-  console.log('handleDisableSpeakingUser');
+async function handleBlacklistAddDisableMsg(userId) {
+  const res = await fetchBlacklistAddDisableMsg({ user_id: userId });
+  if (res.code === 200) {
+    window.$message.success('禁言成功');
+  }
+}
+
+async function handleBlacklistAddAdminDisable(userId) {
+  const res = await fetchBlacklistAddAdminDisable({ user_id: userId });
+  if (res.code === 200) {
+    window.$message.success('禁用成功');
+  }
+}
+
+async function handleBlacklistDelAdminDisable(userId) {
+  const res = await fetchBlacklistDelAdminDisable({ user_id: userId });
+  if (res.code === 200) {
+    window.$message.success('解除禁用成功');
+  }
 }
 
-function handleCancelDisableSpeakingUser() {
-  console.log('handleCancelDisableSpeakingUser');
+async function handleBlacklistDelDisableMsg(userId) {
+  const res = await fetchBlacklistDelDisableMsg({ user_id: userId });
+  if (res.code === 200) {
+    window.$message.success('解除禁言成功');
+  }
 }
 
 function getBg() {
@@ -950,7 +1019,7 @@ function handleScrollTop() {
   display: flex;
   justify-content: space-around;
   margin: 15px auto 0;
-  width: $w-1200;
+  width: $w-1300;
 
   .bg-img-wrap {
     position: absolute;
@@ -991,8 +1060,7 @@ function handleScrollTop() {
     display: inline-block;
     // overflow: hidden;
     box-sizing: border-box;
-    width: $w-900;
-    height: 740px;
+    width: $w-1000;
     border-radius: 6px;
     background-color: $theme-color-papayawhip;
     color: #61666d;
@@ -1002,7 +1070,7 @@ function handleScrollTop() {
       justify-content: space-between;
       box-sizing: border-box;
       padding: 10px 20px;
-      height: 80px;
+      height: 90px;
       color: #18191c;
 
       .info {
@@ -1072,7 +1140,32 @@ function handleScrollTop() {
           display: flex;
           margin-bottom: 10px;
           .item {
+            display: flex;
+            align-items: center;
             margin-right: 10px;
+            font-size: 12px;
+
+            user-select: none;
+            &:hover {
+              color: $theme-color-gold;
+            }
+            &.hover {
+              cursor: pointer;
+            }
+            .ico {
+              margin-right: 4px;
+              width: 15px;
+              height: 15px;
+              &.browse {
+                @include setBackground('@/assets/img/browse.png');
+              }
+              &.good {
+                @include setBackground('@/assets/img/good.png');
+              }
+              &.share {
+                @include setBackground('@/assets/img/share.png');
+              }
+            }
           }
         }
         .bottom {
@@ -1097,7 +1190,8 @@ function handleScrollTop() {
       overflow: hidden;
       align-items: center;
       justify-content: space-between;
-      height: calc(100% - 80px - 100px);
+      width: $w-1000;
+      height: calc($w-1000 / $video-ratio);
       background-color: rgba($color: #000000, $alpha: 0.5);
       .remote-video {
         position: relative;
@@ -1152,10 +1246,6 @@ function handleScrollTop() {
     }
 
     .gift-list {
-      position: absolute;
-      right: 0;
-      bottom: 0;
-      left: 0;
       display: flex;
       align-items: center;
       justify-content: space-around;
@@ -1240,7 +1330,6 @@ function handleScrollTop() {
     display: inline-block;
     box-sizing: border-box;
     width: $w-250;
-    height: 740px;
     border-radius: 6px;
     background-color: $theme-color-papayawhip;
     color: #9499a0;
@@ -1473,7 +1562,7 @@ function handleScrollTop() {
         display: none;
       }
       .video-wrap {
-        height: calc(100% - 100px);
+        height: calc($w-1100 / $video-ratio);
         .remote-video {
           :deep(video) {
             max-width: 100%;
@@ -1515,17 +1604,21 @@ function handleScrollTop() {
 }
 
 // 屏幕宽度大于1500的时候
-@media screen and (min-width: $w-1500) {
+@media screen and (min-width: $w-1600) {
   .pull-wrap {
-    width: $w-1450;
+    width: $w-1500;
 
     .left {
-      width: $w-1100;
+      width: $w-1150;
+      .video-wrap {
+        width: $w-1150;
+        height: calc($w-1150 / $video-ratio);
+      }
       :deep(video) {
-        max-width: $w-1100;
+        max-width: $w-1150;
       }
       :deep(canvas) {
-        max-width: $w-1100;
+        max-width: $w-1150;
       }
     }
     .right {

+ 63 - 13
src/views/push/index.vue

@@ -65,12 +65,16 @@
         >
           <n-space>
             <n-button
-              v-for="(item, index) in allMediaTypeList"
+              v-for="(item, index) in objectSort({
+                obj: allMediaTypeList,
+                sortField: 'priority',
+                sort: 'asc',
+              })"
               :key="index"
               class="item"
-              @click="handleStartMedia(item)"
+              @click="handleStartMedia(item[1] as any)"
             >
-              {{ item.txt }}
+              {{ (item[1] as any).txt }}
             </n-button>
           </n-space>
         </div>
@@ -571,6 +575,7 @@ import {
   getLiveRoomPageUrl,
   getRandomEnglishString,
   handleUserMedia,
+  objectSort,
   readFile,
   saveFile,
   setAudioTrackContentHints,
@@ -1095,7 +1100,7 @@ onUnmounted(() => {
 function handleSendGetLiveUser(liveRoomId: number) {
   clearInterval(loopGetLiveUserTimer.value);
   async function main() {
-    const res = await fetchLiveRoomOnlineUser({ live_room_id: liveRoomId });
+    const res = await fetchLiveRoomOnlineUser(liveRoomId);
     if (res.code === 200) {
       liveUserList.value = res.data;
     }
@@ -1911,7 +1916,7 @@ async function handleCache() {
       obj.trackid = event.getAudioTracks()[0].id;
     }
 
-    async function handleCamera() {
+    async function handleCamera(removeGreen?: boolean) {
       const event = await navigator.mediaDevices.getUserMedia({
         video: { deviceId: obj.deviceId },
         audio: false,
@@ -1921,7 +1926,7 @@ async function handleCache() {
       videoEl.setAttribute('videoid', obj.id);
       videoEl.srcObject = event;
       await new Promise((resolve) => {
-        videoEl.onloadedmetadata = () => {
+        videoEl.onloadedmetadata = async () => {
           const stream = videoEl
             // @ts-ignore
             .captureStream();
@@ -1929,8 +1934,12 @@ async function handleCache() {
           const height = stream.getVideoTracks()[0].getSettings().height!;
           videoEl.width = width;
           videoEl.height = height;
+          let removeGreenCanvas: any = videoEl;
+          if (removeGreen) {
+            removeGreenCanvas = await videoRemoveBackground({ videoEl });
+          }
           const canvasDom = markRaw(
-            new fabric.Image(videoEl, {
+            new fabric.Image(removeGreenCanvas, {
               top: (item.rect?.top || 0) / window.devicePixelRatio,
               left: (item.rect?.left || 0) / window.devicePixelRatio,
               width,
@@ -2025,10 +2034,10 @@ async function handleCache() {
         obj.canvasDom = canvasDom;
       }
     } else if (
-      item.type === MediaTypeEnum.removeGreenVideo &&
+      item.type === MediaTypeEnum.mediaRemoveGreen &&
       item.video === 1
     ) {
-      queue.push(handleMediaVideo(true));
+      queue.push(handleCamera(true));
     }
     res.push(obj);
   });
@@ -2071,6 +2080,7 @@ function setScaleInfo({ track, canvasDom, scale = 1 }) {
 }
 
 async function addMediaOk(val: AppRootState['allTrack'][0]) {
+  console.log('addMediaOk', val);
   showMediaModalCpt.value = false;
   if (val.type === MediaTypeEnum.screen) {
     const event = await handleDisplayMedia({
@@ -2173,6 +2183,46 @@ async function addMediaOk(val: AppRootState['allTrack'][0]) {
     videoTrack.videoEl = videoEl;
     videoTrack.canvasDom = canvasDom;
 
+    const res = [...appStore.allTrack, videoTrack];
+    appStore.setAllTrack(res);
+    cacheStore.setResourceList(res);
+    console.log('获取摄像头成功');
+  } else if (val.type === MediaTypeEnum.cameraRemoveGreen) {
+    const event = await handleUserMedia({
+      video: {
+        deviceId: val.deviceId,
+      },
+      audio: false,
+    });
+    if (!event) return;
+    const videoTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(6),
+      openEye: true,
+      deviceId: val.deviceId,
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.cameraRemoveGreen,
+      track: event.getVideoTracks()[0],
+      trackid: event.getVideoTracks()[0].id,
+      stream: event,
+      streamid: event.id,
+      hidden: false,
+      muted: false,
+      scaleInfo: {},
+      rect: { left: 0, top: 0 },
+    };
+    const { canvasDom, videoEl, scale } = await autoCreateVideo({
+      stream: event,
+      id: videoTrack.id,
+      rect: videoTrack.rect,
+      scaleInfo: videoTrack.scaleInfo,
+      removeGreen: true,
+    });
+    setScaleInfo({ canvasDom, track: videoTrack, scale });
+    videoTrack.videoEl = videoEl;
+    videoTrack.canvasDom = canvasDom;
+
     const res = [...appStore.allTrack, videoTrack];
     appStore.setAllTrack(res);
     cacheStore.setResourceList(res);
@@ -2546,14 +2596,14 @@ async function addMediaOk(val: AppRootState['allTrack'][0]) {
     // @ts-ignore
     cacheStore.setResourceList(res);
     console.log('获取视频成功');
-  } else if (val.type === MediaTypeEnum.removeGreenVideo) {
+  } else if (val.type === MediaTypeEnum.mediaRemoveGreen) {
     const mediaVideoTrack: AppRootState['allTrack'][0] = {
       id: getRandomEnglishString(6),
       openEye: true,
       audio: 2,
       video: 1,
       mediaName: val.mediaName,
-      type: MediaTypeEnum.removeGreenVideo,
+      type: MediaTypeEnum.mediaRemoveGreen,
       track: undefined,
       trackid: undefined,
       stream: undefined,
@@ -2594,7 +2644,7 @@ async function addMediaOk(val: AppRootState['allTrack'][0]) {
           audio: 1,
           video: 2,
           mediaName: val.mediaName,
-          type: MediaTypeEnum.removeGreenVideo,
+          type: MediaTypeEnum.mediaRemoveGreen,
           track: stream.getAudioTracks()[0],
           trackid: stream.getAudioTracks()[0].id,
           stream,
@@ -2615,7 +2665,7 @@ async function addMediaOk(val: AppRootState['allTrack'][0]) {
     appStore.setAllTrack(res);
     // @ts-ignore
     cacheStore.setResourceList(res);
-    console.log('获取视频成功');
+    console.log('获取mediaRemoveGreen成功');
   }
 }
 

+ 64 - 11
src/views/push/mediaModal/index.vue

@@ -90,7 +90,7 @@
             </div>
           </div>
         </template>
-        <template v-if="props.mediaType === MediaTypeEnum.removeGreenVideo">
+        <template v-if="props.mediaType === MediaTypeEnum.mediaRemoveGreen">
           <div class="item">
             <div class="label">视频(移除绿幕)</div>
             <div class="value">
@@ -104,6 +104,32 @@
             </div>
           </div>
         </template>
+        <template v-if="props.mediaType === MediaTypeEnum.cameraRemoveGreen">
+          <!-- <div class="item">
+            <div class="label">摄像头(移除绿幕)</div>
+            <div class="value">
+              <n-upload
+                :max="1"
+                :accept="'video/mp4, video/quicktime'"
+                :on-update:file-list="changMedia"
+              >
+                <n-button :disabled="isEdit">选择文件</n-button>
+              </n-upload>
+            </div>
+          </div> -->
+          <div class="item">
+            <!-- <div class="label">摄像头(移除绿幕)</div> -->
+            <!-- <div class="value">
+              <n-upload
+                :max="1"
+                :accept="'video/mp4, video/quicktime'"
+                :on-update:file-list="changMedia"
+              >
+                <n-button :disabled="isEdit">选择文件</n-button>
+              </n-upload>
+            </div> -->
+          </div>
+        </template>
       </div>
 
       <template #footer>
@@ -171,13 +197,13 @@ function changMedia(list: UploadFileInfo[]) {
 }
 
 function handleOk() {
-  if (mediaName.value.length < 4 || mediaName.value.length > 10) {
-    window.$message.info('名称要求4-10个字符!');
+  if (mediaName.value.length < 4 || mediaName.value.length > 20) {
+    window.$message.info('名称要求4-20个字符!');
     return;
   }
   if (props.mediaType === MediaTypeEnum.txt) {
-    if (txtInfo.value!.txt!.length! < 3 || txtInfo.value!.txt!.length! > 100) {
-      window.$message.info('内容要求3-100个字符!');
+    if (txtInfo.value!.txt!.length! < 1 || txtInfo.value!.txt!.length! > 100) {
+      window.$message.info('内容要求1-100个字符!');
       return;
     }
   }
@@ -194,6 +220,12 @@ function handleOk() {
         return;
       }
     }
+    if (props.mediaType === MediaTypeEnum.mediaRemoveGreen) {
+      if (mediaInfo.value!.length! !== 1) {
+        window.$message.info('请选择视频!');
+        return;
+      }
+    }
   }
   if (props.isEdit) {
     emits('editOk', {
@@ -327,23 +359,44 @@ async function init() {
         .filter((item) => item.type === MediaTypeEnum.media)
         .filter((item) => !item.hidden).length + 1
     }`;
-  } else if (props.mediaType === MediaTypeEnum.removeGreenVideo) {
+  } else if (props.mediaType === MediaTypeEnum.mediaRemoveGreen) {
     currentInput.value = {
       ...currentInput.value,
-      type: MediaTypeEnum.removeGreenVideo,
+      type: MediaTypeEnum.mediaRemoveGreen,
     };
     mediaInfo.value = [];
     mediaName.value = `视频(移除绿幕)-${
       appStore.allTrack
-        .filter((item) => item.type === MediaTypeEnum.removeGreenVideo)
+        .filter((item) => item.type === MediaTypeEnum.mediaRemoveGreen)
+        .filter((item) => !item.hidden).length + 1
+    }`;
+  } else if (props.mediaType === MediaTypeEnum.cameraRemoveGreen) {
+    res.forEach((item) => {
+      if (item.kind === 'videoinput' && item.deviceId !== 'default') {
+        inputOptions.value.push({
+          label: item.label,
+          value: item.deviceId,
+        });
+      }
+    });
+    currentInput.value = {
+      ...currentInput.value,
+      deviceId: inputOptions.value[0].value,
+      type: MediaTypeEnum.cameraRemoveGreen,
+    };
+    mediaName.value = `摄像头(移除绿幕)-${
+      appStore.allTrack
+        .filter((item) => item.type === MediaTypeEnum.cameraRemoveGreen)
         .filter((item) => !item.hidden).length + 1
     }`;
   }
   if (props.initData) {
     if (
-      [MediaTypeEnum.camera, MediaTypeEnum.microphone].includes(
-        props.initData.type
-      )
+      [
+        MediaTypeEnum.camera,
+        MediaTypeEnum.cameraRemoveGreen,
+        MediaTypeEnum.microphone,
+      ].includes(props.initData.type)
     ) {
       currentInput.value = {
         deviceId: props.initData.deviceId!,

+ 18 - 6
src/views/push/selectMediaModal/index.vue

@@ -2,18 +2,18 @@
   <div class="select-media-wrap">
     <Modal
       title="选择直播素材"
-      :mask-closable="false"
+      :mask-closable="true"
       @close="emits('close')"
-      :width="'350px'"
+      :width="'520px'"
     >
       <div class="btn-wrap">
         <div
           class="btn"
-          v-for="(item, index) in allMediaTypeList"
+          v-for="(item, index) in obj"
           :key="index"
         >
-          <n-button @click="emits('ok', item.type)">
-            {{ item.txt }}
+          <n-button @click="emits('ok', item[1].type)">
+            {{ item[1].txt }}
           </n-button>
         </div>
       </div>
@@ -24,9 +24,12 @@
 </template>
 
 <script lang="ts" setup>
+import { onMounted, ref } from 'vue';
+
 import { MediaTypeEnum } from '@/interface';
+import { objectSort } from '@/utils';
 
-withDefaults(
+const props = withDefaults(
   defineProps<{
     allMediaTypeList: {
       [index: string]: {
@@ -38,6 +41,15 @@ withDefaults(
   {}
 );
 const emits = defineEmits(['close', 'ok']);
+
+const obj = ref();
+onMounted(() => {
+  obj.value = objectSort({
+    obj: props.allMediaTypeList,
+    sortField: 'priority',
+    sort: 'asc',
+  });
+});
 </script>
 
 <style lang="scss" scoped>

+ 2 - 1
src/views/rank/index.vue

@@ -607,8 +607,9 @@ onMounted(() => {
             border-radius: 50%;
           }
           .username {
+            margin-right: 15px;
             margin-left: 10px;
-            width: 100px;
+            max-width: 200px;
 
             @extend %singleEllipsis;
           }

+ 0 - 67
test/test.html

@@ -1,67 +0,0 @@
-<!doctype html>
-<html lang="en">
-  <head>
-    <meta charset="UTF-8" />
-    <meta
-      name="viewport"
-      content="width=device-width, initial-scale=1.0"
-    />
-    <title>Video to ReadableStream</title>
-  </head>
-  <body>
-    <video
-      id="video"
-      width="640"
-      height="480"
-      autoplay
-      loop
-      muted
-    >
-      <source
-        src="your-video.mp4"
-        type="video/mp4"
-      />
-    </video>
-    <script>
-      const video = document.getElementById('video');
-
-      async function videoToStream(videoElement) {
-        // 创建 MediaStream
-        const stream = videoElement.captureStream();
-        const reader = new MediaRecorder(stream);
-
-        // 创建一个可读流
-        const readableStream = new ReadableStream({
-          start(controller) {
-            reader.ondataavailable = (event) => {
-              if (event.data.size > 0) {
-                const chunk = new Uint8Array();
-                const reader = new FileReader();
-                reader.onload = () => {
-                  // 将数据块添加到可读流
-                  controller.enqueue(new Uint8Array(reader.result));
-                };
-                reader.readAsArrayBuffer(event.data);
-              }
-            };
-            reader.onstop = () => {
-              controller.close();
-            };
-            // 开始录制
-            reader.start();
-          },
-          cancel() {
-            reader.stop();
-          },
-        });
-
-        return readableStream;
-      }
-
-      video.addEventListener('loadeddata', async () => {
-        const stream = await videoToStream(video);
-        console.log(stream); // 你可以在这里使用这个流
-      });
-    </script>
-  </body>
-</html>

+ 0 - 13
test/test.js

@@ -1,13 +0,0 @@
-async function demo() {
-  let res1 = await Promise.resolve('123');
-  return res1;
-}
-
-async function main() {
-  const res = await Promise.all([demo()]);
-  const res1 = await Promise.resolve(demo());
-  console.log(res);
-  console.log(res1);
-}
-
-main();

+ 41 - 11
test/test.ts

@@ -1,15 +1,45 @@
-enum pet {
-  a = 1,
-  b,
-}
+var allMediaTypeList = {
+  a: {
+    txt: '摄像头',
+    priority: 1,
+  },
+  b: {
+    txt: 'dfs',
+    priority: 21,
+  },
+  c: {
+    txt: '32',
+    priority: 2,
+  },
+  d: {
+    txt: 'abc',
+    priority: 2,
+  },
+};
+
+function objectSort(data: { obj; sortField; sort?: 'asc' | 'desc' }) {
+  // 将对象转换为数组
+  var entries = Object.entries(data.obj);
+  entries.sort(function (a, b) {
+    // @ts-ignore
+    var res1 = a[1][data.sortField];
+    // @ts-ignore
+    var res2 = b[1][data.sortField];
+    if (data.sort === 'desc') {
+      return res2 - res1;
+    } else {
+      return res1 - res2;
+    }
+  });
 
-function test(d: pet) {
-  console.log(d.a, d['a'], 'sdsd');
-  if (1 === d.a) {
-    console.log('111');
-  } else {
-    console.log('222');
+  // 将排序后的数组转换回对象
+  var sortedMediaTypeList = {};
+  for (var i = 0; i < entries.length; i++) {
+    sortedMediaTypeList[entries[i][0]] = entries[i][1];
   }
+  return sortedMediaTypeList;
 }
 
-test(pet.a);
+console.log(
+  objectSort({ obj: allMediaTypeList, sortField: 'priority', sort: 'desc' })
+);