Kaynağa Gözat

feat: 禁用eslint以优化开发体验

shuisheng 2 yıl önce
ebeveyn
işleme
d8f6191615

+ 34 - 17
.eslintrc.js

@@ -1,30 +1,43 @@
 console.log(
   '\x1B[0;37;44m INFO \x1B[0m',
   '\x1B[0;;34m ' +
-    `读取了: ${__filename.slice(__dirname.length + 1)}` +
+    `${new Date().toLocaleString()}读取了: ${__filename.slice(
+      __dirname.length + 1
+    )}` +
     ' \x1B[0m'
 );
 
 module.exports = {
   root: true,
   settings: {
-    // 'import/extensions': ['.js', '.jsx', '.ts', '.tsx'],
+    'import/parsers': {
+      '@typescript-eslint/parser': ['.ts', '.tsx'],
+    },
+    'import/resolver': {
+      typescript: {
+        alwaysTryTypes: true, // always try to resolve types under `<root>@types` directory even it doesn't contain any source code, like `@types/unist`
+        project: './tsconfig.json',
+      },
+    },
   },
   env: {
     browser: true,
     node: true,
   },
+
   extends: [
     // 'airbnb-base', // airbnb的eslint规范,它会对import和require进行排序,挺好的。如果不用它的话,需要在env添加node:true
     'eslint:recommended',
     'plugin:import/recommended',
-    'plugin:vue/vue3-recommended',
-    // '@vue/eslint-config-typescript', // 启用这个规则后,vscode保存文件时格式化很慢
-    '@vue/eslint-config-typescript/recommended', // 启用这个规则后,vscode保存文件时格式化很慢
+    'plugin:vue/vue3-essential', // plugin:vue/vue3-essential或plugin:vue/vue3-strongly-recommended或plugin:vue/vue3-recommended'
+    '@vue/eslint-config-typescript', // 启用这个规则后,vscode保存文件时格式化很慢
+    // '@vue/eslint-config-typescript/recommended', // 启用这个规则后,vscode保存文件时格式化很慢
     '@vue/eslint-config-prettier',
   ],
+  parser: 'vue-eslint-parser',
   parserOptions: {
-    ecmaVersion: 2020,
+    parser: '@typescript-eslint/parser',
+    ecmaVersion: 2022,
     tsconfigRootDir: __dirname, // https://typescript-eslint.io/docs/linting/typed-linting
     project: ['./tsconfig.json'], // https://typescript-eslint.io/docs/linting/typed-linting
   },
@@ -126,13 +139,7 @@ module.exports = {
     ],
     'import/newline-after-import': 2, // 强制在最后一个顶级导入语句或 require 调用之后有一个或多个空行
     'import/no-extraneous-dependencies': 2, // 禁止导入未在package.json中声明的外部模块。
-    /**
-     * import/named
-     * 在import { version } from 'vuex';的时候会验证vuex有没有具名导出version,
-     * 但是在vue3的时候,import { defineComponent } from 'vue';会报错defineComponent not found in 'vue'
-     * 因此vue3项目关闭该规则
-     */
-    'import/named': 0,
+
     /**
      * a.js
      * export const version = '1.0.0';
@@ -143,9 +150,18 @@ module.exports = {
      * console.log(bar.version); // 检测到你使用的version有具名导出,import/no-named-as-default-member就会提示`import {version} from './a'`
      */
     'import/no-named-as-default-member': 1, // https://github.com/import-js/eslint-plugin-import/blob/v2.26.0/docs/rules/no-named-as-default-member.md
-    'import/prefer-default-export': 0, // 当模块只有一个导出时,更喜欢使用默认导出而不是命名导出。
-    'import/extensions': 0, // 确保在导入路径中一致使用文件扩展名。在js/ts等文件里引其他文件都不能带后缀(比如.css和.jpg),因此关掉
-    'import/no-unresolved': 0, // 不能解析带别名的路径的模块,但实际上是不影响代码运行的。找不到解决办法,暂时关掉。
+
+    /**
+     * import/named
+     * 在import { version } from 'vuex';的时候会验证vuex有没有具名导出version,
+     * 但是在vue3的时候,import { defineComponent } from 'vue';会报错defineComponent not found in 'vue'
+     * 因此vue3项目关闭该规则
+     */
+    // 'import/named': 0,
+    // 'import/prefer-default-export': 0, // 当模块只有一个导出时,更喜欢使用默认导出而不是命名导出。
+    // 'import/extensions': 0, // 确保在导入路径中一致使用文件扩展名。在js/ts等文件里引其他文件都不能带后缀(比如.css和.jpg),因此关掉
+    // 'import/no-unresolved': 0, // 不能解析带别名的路径的模块,但实际上是不影响代码运行的。找不到解决办法,暂时关掉。
+
     /**
      * a.js
      * export const bar = 'bar';
@@ -159,6 +175,8 @@ module.exports = {
     'import/no-named-as-default': 0, // https://github.com/import-js/eslint-plugin-import/blob/v2.26.0/docs/rules/no-named-as-default.md
 
     // @typescript-eslint插件
+    '@typescript-eslint/no-unused-vars': 2,
+
     '@typescript-eslint/restrict-template-expressions': [
       'error',
       {
@@ -166,7 +184,6 @@ module.exports = {
         allowNumber: true,
       },
     ], // 强制模板文字表达式为string类型。即const a = {};console.log(`${a}`);会报错
-    '@typescript-eslint/no-unused-vars': 2,
     '@typescript-eslint/no-floating-promises': 0, // 要求适当处理类似 Promise 的语句。即将await或者return Promise,或者对promise进行.then或者.catch
     '@typescript-eslint/no-explicit-any': 0, // 不允许定义any类型。即let a: any;会报错
     '@typescript-eslint/no-non-null-assertion': 0, // 禁止使用非空断言(后缀运算符!)。即const el = document.querySelector('.app');console.log(el!.tagName);会报错

+ 8 - 2
.vscode/settings.json

@@ -2,13 +2,19 @@
   // 指定行尾序列为\n(LF)或者\r\n(CRLF)或者auto
   // "files.eol": "\n",
 
+  "vue.codeActions.savingTimeLimit": 500,
+
   // 在保存时格式化
   "editor.formatOnSave": true,
+  "editor.formatOnSaveMode": "file",
+
+  "eslint.codeActionsOnSave.mode": "problems",
+  "eslint.debug": false,
 
   // 保存时进行一些操作
   "editor.codeActionsOnSave": {
-    "source.fixAll.eslint": true, // 运行eslint
-    "source.organizeImports": true // 整理import语句(包括import的成员),以及会删掉未使用的导入,注意:会删掉declare global {import utils from 'billd-utils';}的import utils from 'billd-utils';
+    "source.fixAll.eslint": false, // 运行eslint
+    "source.organizeImports": false // 整理import语句(包括import的成员),以及会删掉未使用的导入,注意:会删掉declare global {import utils from 'billd-utils';}的import utils from 'billd-utils';
     // "source.sortImports": true // 对您的导入进行排序,然而,与organizeImports不同,它不会删除任何未使用的导入,也不会对import里面的成员进行排序
   },
 

+ 9 - 14
package.json

@@ -32,31 +32,27 @@
     }
   },
   "dependencies": {
-    "@types/fabric": "^5.3.3",
     "@vicons/ionicons5": "^0.12.0",
     "axios": "^1.2.1",
     "billd-html-webpack-plugin": "^1.0.4",
     "billd-scss": "^0.0.7",
     "billd-utils": "^0.0.12",
     "browser-tool": "^1.0.5",
+    "esbuild-loader": "^3.1.0",
+    "eslint-import-resolver-typescript": "^3.5.5",
     "fabric": "^5.3.0",
     "flv.js": "^1.6.2",
     "js-cookie": "^3.0.5",
-    "m3u8-parser": "^6.2.0",
-    "mediainfo.js": "^0.1.9",
-    "mediasoup-client": "^3.6.84",
     "mpegts.js": "^1.7.3",
-    "msr": "^1.3.4",
     "naive-ui": "^2.34.3",
     "pinia": "^2.0.33",
     "pinia-plugin-persistedstate": "^3.2.0",
     "qrcode": "^1.5.3",
-    "sdp-transform": "^2.14.1",
     "socket.io-client": "^4.6.1",
     "unplugin-vue-components": "^0.24.1",
     "vconsole": "^3.15.0",
     "video.js": "^8.3.0",
-    "vue": "^3.2.31",
+    "vue": "^3.3.4",
     "vue-demi": "^0.13.11",
     "vue-router": "^4.0.13",
     "webrtc-adapter": "^8.2.2",
@@ -69,12 +65,12 @@
     "@commitlint/config-conventional": "^16.0.0",
     "@rushstack/eslint-patch": "^1.1.0",
     "@soda/friendly-errors-webpack-plugin": "^1.8.1",
+    "@types/fabric": "^5.3.3",
     "@types/node": "^18.11.9",
     "@types/video.js": "^7.3.52",
-    "@typescript-eslint/parser": "^5.8.1",
     "@vue/compiler-sfc": "^3.2.31",
-    "@vue/eslint-config-prettier": "^7.0.0",
-    "@vue/eslint-config-typescript": "^10.0.0",
+    "@vue/eslint-config-prettier": "^8.0.0",
+    "@vue/eslint-config-typescript": "^11.0.3",
     "@vue/preload-webpack-plugin": "^2.0.0",
     "babel-loader": "^8.2.2",
     "chalk": "^4",
@@ -88,8 +84,7 @@
     "cz-conventional-changelog": "^3.3.0",
     "cz-customizable": "^7.0.0",
     "eslint": "^8.13.0",
-    "eslint-plugin-import": "^2.27.5",
-    "eslint-plugin-vue": "^8.5.0",
+    "eslint-plugin-import": "^2.28.0",
     "eslint-webpack-plugin": "^3.1.1",
     "file-loader": "^6.2.0",
     "fork-ts-checker-webpack-plugin": "^7.2.6",
@@ -103,7 +98,7 @@
     "postcss": "^8.4.8",
     "postcss-loader": "^6.2.1",
     "postcss-preset-env": "^7.4.2",
-    "prettier": "^2.5.1",
+    "prettier": "^3.0.1",
     "sass": "^1.45.2",
     "sass-loader": "^12.4.0",
     "standard-version": "^9.3.2",
@@ -112,7 +107,7 @@
     "thread-loader": "^3.0.4",
     "ts-loader": "^9.2.7",
     "ts-node": "^10.9.1",
-    "typescript": "^4.6.2",
+    "typescript": "^5.1.6",
     "vue-loader": "^17.0.0",
     "vue-style-loader": "^4.1.3",
     "webpack": "^5.68.0",

Dosya farkı çok büyük olduğundan ihmal edildi
+ 361 - 140
pnpm-lock.yaml


+ 58 - 58
script/config/webpack.common.ts

@@ -189,64 +189,64 @@ const commonConfig = (isProduction) => {
             },
           ],
         },
-        {
-          test: /\.jsx?$/,
-          exclude: /node_modules/,
-          use: [
-            // 'thread-loader',
-            {
-              loader: 'babel-loader',
-              options: {
-                cacheDirectory: true,
-                cacheCompression: false, // https://github.com/facebook/create-react-app/issues/6846
-              },
-            },
-          ],
-        },
-        {
-          test: /\.ts$/,
-          exclude: /node_modules/,
-          use: [
-            {
-              loader: 'babel-loader',
-              options: {
-                cacheDirectory: true,
-                cacheCompression: false, // https://github.com/facebook/create-react-app/issues/6846
-              },
-            },
-            {
-              loader: 'ts-loader',
-              options: {
-                appendTsSuffixTo: ['\\.vue$'],
-                // If you want to speed up compilation significantly you can set this flag. https://www.npmjs.com/package/ts-loader#transpileonly
-                transpileOnly: true,
-                happyPackMode: false,
-              },
-            },
-          ],
-        },
-        {
-          test: /\.tsx$/,
-          exclude: /node_modules/,
-          use: [
-            {
-              loader: 'babel-loader',
-              options: {
-                cacheDirectory: true,
-                cacheCompression: false, // https://github.com/facebook/create-react-app/issues/6846
-              },
-            },
-            {
-              loader: 'ts-loader',
-              options: {
-                appendTsxSuffixTo: ['\\.vue$'],
-                // If you want to speed up compilation significantly you can set this flag. https://www.npmjs.com/package/ts-loader#transpileonly
-                transpileOnly: true,
-                happyPackMode: false,
-              },
-            },
-          ],
-        },
+        // {
+        //   test: /\.jsx?$/,
+        //   exclude: /node_modules/,
+        //   use: [
+        //     // 'thread-loader',
+        //     {
+        //       loader: 'babel-loader',
+        //       options: {
+        //         cacheDirectory: true,
+        //         cacheCompression: false, // https://github.com/facebook/create-react-app/issues/6846
+        //       },
+        //     },
+        //   ],
+        // },
+        // {
+        //   test: /\.ts$/,
+        //   exclude: /node_modules/,
+        //   use: [
+        //     {
+        //       loader: 'babel-loader',
+        //       options: {
+        //         cacheDirectory: true,
+        //         cacheCompression: false, // https://github.com/facebook/create-react-app/issues/6846
+        //       },
+        //     },
+        //     {
+        //       loader: 'ts-loader',
+        //       options: {
+        //         appendTsSuffixTo: ['\\.vue$'],
+        //         // If you want to speed up compilation significantly you can set this flag. https://www.npmjs.com/package/ts-loader#transpileonly
+        //         transpileOnly: true,
+        //         happyPackMode: false,
+        //       },
+        //     },
+        //   ],
+        // },
+        // {
+        //   test: /\.tsx$/,
+        //   exclude: /node_modules/,
+        //   use: [
+        //     {
+        //       loader: 'babel-loader',
+        //       options: {
+        //         cacheDirectory: true,
+        //         cacheCompression: false, // https://github.com/facebook/create-react-app/issues/6846
+        //       },
+        //     },
+        //     {
+        //       loader: 'ts-loader',
+        //       options: {
+        //         appendTsxSuffixTo: ['\\.vue$'],
+        //         // If you want to speed up compilation significantly you can set this flag. https://www.npmjs.com/package/ts-loader#transpileonly
+        //         transpileOnly: true,
+        //         happyPackMode: false,
+        //       },
+        //     },
+        //   ],
+        // },
         {
           test: /\.css$/,
           oneOf: [

+ 46 - 10
script/config/webpack.dev.ts

@@ -24,17 +24,23 @@ export default new Promise((resolve) => {
         // https://github.com/webpack/webpack/blob/main/lib/config/defaults.js
         mode: 'development',
         stats: 'none',
-        cache: {
-          type: 'filesystem',
-          buildDependencies: {
-            // https://webpack.js.org/configuration/cache/#cacheallowcollectingmemory
-            // 建议cache.buildDependencies.config: [__filename]在您的 webpack 配置中设置以获取最新配置和所有依赖项。
-            config: [__filename],
-          },
-        },
+        // cache: {
+        //   type: 'filesystem',
+        //   allowCollectingMemory: true, // 它在生产模式中默认为false,并且在开发模式下默认为true。https://webpack.js.org/configuration/cache/#cacheallowcollectingmemory
+        //   buildDependencies: {
+        //     // 建议cache.buildDependencies.config: [__filename]在您的 webpack 配置中设置以获取最新配置和所有依赖项。
+        //     config: [
+        //       resolveApp('./script/config/webpack.common.ts'),
+        //       resolveApp('./script/config/webpack.dev.ts'),
+        //       resolveApp('./script/config/webpack.prod.ts'),
+        //       resolveApp('.browserslistrc'), // 防止修改了.browserslistrc文件后,但没修改webpack配置文件,webpack不读取最新更新后的.browserslistrc
+        //       resolveApp('babel.config.js'), // 防止修改了babel.config.js文件后,但没修改webpack配置文件,webpack不读取最新更新后的babel.config.js
+        //     ],
+        //   },
+        // },
         // https://webpack.docschina.org/configuration/devtool/
-        devtool: 'eval-cheap-module-source-map',
-        // devtool: 'eval', // eval,具有最高性能的开发构建的推荐选择。
+        // devtool: 'eval-cheap-module-source-map',
+        devtool: 'eval', // eval,具有最高性能的开发构建的推荐选择。
         // 这个infrastructureLogging设置参考了vuecli5,如果不设置,webpack-dev-server会打印一些信息
         infrastructureLogging: {
           level: 'none',
@@ -97,6 +103,36 @@ export default new Promise((resolve) => {
             },
           },
         },
+        module: {
+          rules: [
+            {
+              test: /\.jsx?$/,
+              exclude: /node_modules/,
+              use: [
+                {
+                  loader: 'esbuild-loader',
+                  options: {
+                    loader: 'jsx', // Remove this if you're not using JSX
+                    target: 'esnext', // Syntax to compile to (see options below for possible values)
+                  },
+                },
+              ],
+            },
+            {
+              test: /\.tsx?$/,
+              exclude: /node_modules/,
+              use: [
+                {
+                  loader: 'esbuild-loader',
+                  options: {
+                    loader: 'tsx', // Remove this if you're not using JSX
+                    target: 'esnext', // Syntax to compile to (see options below for possible values)
+                  },
+                },
+              ],
+            },
+          ],
+        },
         // @ts-ignore
         plugins: [
           // 构建进度条

+ 40 - 0
script/config/webpack.prod.ts

@@ -135,6 +135,46 @@ const prodConfig: Configuration = {
     //   name: 'runtime'
     // }
   },
+  // module: {
+  //   rules: [
+  //     {
+  //       test: /\.jsx?$/,
+  //       exclude: /node_modules/,
+  //       use: [
+  //         // 'thread-loader',
+  //         {
+  //           loader: 'babel-loader',
+  //           options: {
+  //             cacheDirectory: true,
+  //             cacheCompression: false, // https://github.com/facebook/create-react-app/issues/6846
+  //           },
+  //         },
+  //       ],
+  //     },
+  //     {
+  //       test: /\.tsx?$/,
+  //       exclude: /node_modules/,
+  //       use: [
+  //         {
+  //           loader: 'babel-loader',
+  //           options: {
+  //             cacheDirectory: true,
+  //             cacheCompression: false, // https://github.com/facebook/create-react-app/issues/6846
+  //           },
+  //         },
+  //         {
+  //           loader: 'ts-loader',
+  //           options: {
+  //             appendTsxSuffixTo: ['\\.vue$'],
+  //             // If you want to speed up compilation significantly you can set this flag. https://www.npmjs.com/package/ts-loader#transpileonly
+  //             transpileOnly: true,
+  //             happyPackMode: false,
+  //           },
+  //         },
+  //       ],
+  //     },
+  //   ],
+  // },
   plugins: [
     // 构建进度条
     new WebpackBar(),

+ 0 - 2
src/components/Avatar/index.vue

@@ -19,8 +19,6 @@
 </template>
 
 <script lang="ts" setup>
-import { defineProps } from 'vue';
-
 const props = defineProps({
   avatar: { type: String, default: '' },
   size: { type: Number, default: 100 },

+ 1 - 9
src/components/DND/index.vue

@@ -8,15 +8,7 @@
 </template>
 
 <script lang="ts" setup>
-import {
-  defineEmits,
-  defineProps,
-  onMounted,
-  onUnmounted,
-  reactive,
-  ref,
-  withDefaults,
-} from 'vue';
+import { onMounted, onUnmounted, reactive, ref } from 'vue';
 
 const dndRef = ref<HTMLElement>();
 const offset = reactive({ x: 0, y: 0 }); // x:距离最左边多少px;y:距离最下边多少px

+ 3 - 2
src/components/Dropdown/index.vue

@@ -21,7 +21,6 @@
 
 <script lang="ts" setup>
 import { isMobile } from 'billd-utils';
-import { defineProps } from 'vue';
 
 const props = defineProps({
   modelValue: { type: Boolean, default: false },
@@ -53,7 +52,9 @@ const emits = defineEmits(['update:modelValue']);
       padding: 10px 0;
       border-radius: 5px;
       background-color: #fff;
-      box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.08);
+      box-shadow:
+        0 12px 32px rgba(0, 0, 0, 0.1),
+        0 2px 6px rgba(0, 0, 0, 0.08);
       color: black;
       font-size: 14px;
     }

+ 1 - 1
src/components/Modal/index.vue

@@ -28,7 +28,7 @@
 </template>
 
 <script lang="ts" setup>
-import { defineEmits, defineProps, useSlots } from 'vue';
+import { useSlots } from 'vue';
 
 const slots = useSlots();
 const props = defineProps({

+ 1 - 1
src/components/QrPay/index.vue

@@ -48,7 +48,7 @@
 <script lang="ts" setup>
 import { hrefToTarget, isMobile } from 'billd-utils';
 import QRCode from 'qrcode';
-import { defineProps, onMounted, onUnmounted, ref } from 'vue';
+import { onMounted, onUnmounted, ref } from 'vue';
 
 import { fetchAliPay, fetchAliPayStatus } from '@/api/order';
 import { PayStatusEnum } from '@/interface';

+ 1 - 1
src/directives/index.ts

@@ -1,6 +1,6 @@
 import { App } from 'vue';
 
-import directiveLoading from './loading';
+import { directiveLoading } from './loading';
 
 export default function registerDirectives(app: App) {
   app.directive('loading', directiveLoading);

+ 8 - 8
src/directives/loading/index.ts

@@ -10,15 +10,15 @@ const map = new Map<
   }
 >();
 
-export default <Directive>{
+export const directiveLoading: Directive = {
   // 在绑定元素的 attribute 前
   // 或事件监听器应用前调用
-  created() {},
+  // created() {},
   // 在元素被插入到 DOM 前调用
-  beforeMount() {},
+  // beforeMount() {},
   // 在绑定元素的父组件
   // 及他自己的所有子节点都挂载完成后调用
-  mounted(el, binding, vnode) {
+  mounted(el, binding) {
     const { value } = binding;
     const app = createApp(main);
     const container = document.createElement('div');
@@ -31,9 +31,9 @@ export default <Directive>{
     return instance;
   },
   // 绑定元素的父组件更新前调用
-  beforeUpdate() {},
+  // beforeUpdate() {},
   // 在绑定元素的父组件及他自己的所有子节点都更新后调用
-  updated(el, binding, vnode) {
+  updated(el, binding) {
     const { value } = binding;
     const res = map.get(el);
     if (res) {
@@ -41,9 +41,9 @@ export default <Directive>{
     }
   },
   // 绑定元素的父组件卸载前调用
-  beforeUnmount() {},
+  // beforeUnmount() {},
   // 绑定元素的父组件卸载后调用
-  unmounted(el, binding, vnode) {
+  unmounted(el) {
     map.get(el)?.app.unmount();
   },
 };

+ 0 - 1
src/main.ts

@@ -1,7 +1,6 @@
 import './main.scss';
 import './showBilldVersion';
 // import 'windi.css'; // windicss-webpack-plugin会解析windi.css这个MODULE_ID
-
 import { createApp } from 'vue';
 import adapter from 'webrtc-adapter';
 

+ 1 - 1
src/views/push/mediaModal/index.vue

@@ -41,7 +41,7 @@
 </template>
 
 <script lang="ts" setup>
-import { defineEmits, defineProps, onMounted, ref, withDefaults } from 'vue';
+import { onMounted, ref } from 'vue';
 
 import { MediaTypeEnum } from '@/interface';
 import { useAppStore } from '@/store/app';

+ 1 - 1
src/views/push/selectMediaModal/index.vue

@@ -23,7 +23,7 @@
 </template>
 
 <script lang="ts" setup>
-import { defineEmits, defineProps, onMounted, withDefaults } from 'vue';
+import { onMounted } from 'vue';
 
 import { MediaTypeEnum } from '@/interface';
 

+ 3 - 4
src/views/pushByCanvas/index.vue

@@ -475,7 +475,6 @@ function handleMixedAudio() {
   if (audioCtx.value) {
     const gainNode = audioCtx.value.createGain();
     allAudioTrack.forEach((item) => {
-      console.log('llllllll', item);
       if (!audioCtx.value || !item.stream) return;
       const audioInput = audioCtx.value.createMediaStreamSource(item.stream);
       audioInput.connect(gainNode);
@@ -489,7 +488,7 @@ function handleMixedAudio() {
       mixedStream.addTrack(destination.stream.getAudioTracks()[0]);
       mixedStream.addTrack(canvasVideoStream.value!.getVideoTracks()[0]);
       canvasVideoStream.value = mixedStream;
-      console.log('替换了');
+      console.log('替换了21111');
       return;
     }
     const destination = audioCtx.value.createMediaStreamDestination();
@@ -581,9 +580,9 @@ function autoCreateVideo({
     clearFrame: any;
     scale: number;
   }>((resolve) => {
-    console.log(videoEl, 888888888);
+    console.log(videoEl, 888211888888);
     videoEl.onloadedmetadata = () => {
-      console.log('kkkkkkkkkk', 123);
+      console.log('kkkkkkk2kkk', 123);
       const width = stream.getVideoTracks()[0].getSettings().width!;
       const height = stream.getVideoTracks()[0].getSettings().height!;
       const ratio = handleScale({ width, height });

+ 1639 - 0
src/views/pushByCanvas/index2.vue

@@ -0,0 +1,1639 @@
+<template>
+  <div class="push-wrap">
+    <div
+      ref="topRef"
+      class="left"
+    >
+      <div
+        ref="containerRef"
+        class="container"
+      >
+        <canvas
+          id="pushCanvasRef"
+          ref="pushCanvasRef"
+        ></canvas>
+        <div
+          v-if="appStore.allTrack.filter((item) => !item.hidden).length <= 0"
+          class="add-wrap"
+        >
+          <n-space>
+            <n-button
+              v-for="(item, index) in allMediaTypeList"
+              :key="index"
+              class="item"
+              @click="handleStartMedia(item)"
+            >
+              {{ item.txt }}
+            </n-button>
+          </n-space>
+        </div>
+      </div>
+
+      <div
+        ref="bottomRef"
+        class="room-control"
+      >
+        <div class="info">
+          <div
+            class="avatar"
+            :style="{ backgroundImage: `url(${userStore.userInfo?.avatar})` }"
+          ></div>
+          <div class="detail">
+            <div class="top">
+              <n-input-group>
+                <n-input
+                  v-model:value="roomName"
+                  size="small"
+                  placeholder="输入房间名"
+                  :style="{ width: '50%' }"
+                />
+                <n-button
+                  size="small"
+                  type="primary"
+                  @click="confirmRoomName"
+                >
+                  确定
+                </n-button>
+              </n-input-group>
+            </div>
+            <div class="bottom">
+              <span v-if="NODE_ENV === 'development'">
+                {{ getSocketId() }}
+              </span>
+            </div>
+          </div>
+        </div>
+        <div class="rtc">
+          <div class="item">
+            <div class="txt">码率设置</div>
+            <div class="down">
+              <n-select
+                v-model:value="currentMaxBitrate"
+                :options="maxBitrate"
+              />
+            </div>
+          </div>
+          <div class="item">
+            <div class="txt">帧率设置</div>
+            <div class="down">
+              <n-select
+                v-model:value="currentMaxFramerate"
+                :options="maxFramerate"
+              />
+            </div>
+          </div>
+          <div class="item">
+            <div class="txt">分辨率设置</div>
+            <div class="down">
+              <n-select
+                v-model:value="currentResolutionRatio"
+                :options="resolutionRatio"
+              />
+            </div>
+          </div>
+        </div>
+        <div class="other">
+          <div class="top">
+            <span class="item">
+              <i class="ico"></i>
+              <span>
+                正在观看:
+                {{
+                  liveUserList.filter((item) => item.id !== getSocketId())
+                    .length
+                }}
+              </span>
+            </span>
+          </div>
+          <div class="bottom">
+            <n-button
+              v-if="!isLiving"
+              type="info"
+              size="small"
+              @click="handleStartLive"
+            >
+              开始直播
+            </n-button>
+            <n-button
+              v-else
+              type="error"
+              size="small"
+              @click="endLive"
+            >
+              结束直播
+            </n-button>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="right">
+      <div class="resource-card">
+        <div class="title">素材列表</div>
+        <div class="list">
+          <div
+            v-for="(item, index) in appStore.allTrack.filter(
+              (item) => !item.hidden
+            )"
+            :key="index"
+            class="item"
+          >
+            <span class="name">
+              ({{ mediaTypeEnumMap[item.type] }}){{ item.mediaName }}
+            </span>
+            <div class="control">
+              <div
+                v-if="item.audio === 1"
+                class="control-item"
+                @click="handleChangeMuted(item)"
+              >
+                <n-icon size="16">
+                  <VolumeMuteOutline v-if="item.muted"></VolumeMuteOutline>
+                  <VolumeHighOutline v-else></VolumeHighOutline>
+                </n-icon>
+              </div>
+              <div
+                class="control-item"
+                @click="handleEdit(item)"
+              >
+                <n-icon size="16">
+                  <CreateOutline></CreateOutline>
+                </n-icon>
+              </div>
+              <div
+                class="control-item"
+                @click="handleDel(item)"
+              >
+                <n-icon size="16">
+                  <Close></Close>
+                </n-icon>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="bottom">
+          <n-button
+            size="small"
+            type="primary"
+            @click="showSelectMediaModalCpt = true"
+          >
+            添加素材
+          </n-button>
+        </div>
+      </div>
+      <div class="danmu-card">
+        <div class="title">弹幕互动</div>
+        <div class="list-wrap">
+          <div
+            ref="danmuListRef"
+            class="list"
+          >
+            <div
+              v-for="(item, index) in damuList"
+              :key="index"
+              class="item"
+            >
+              <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
+                <span class="name">
+                  {{ item.userInfo?.username || item.socket_id }}:
+                </span>
+                <span class="msg">{{ item.msg }}</span>
+              </template>
+              <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
+                <span class="name system">系统通知:</span>
+                <span class="msg">
+                  <span>{{ item.userInfo?.username || item.socket_id }}</span>
+                  <span>进入直播!</span>
+                </span>
+              </template>
+              <template
+                v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved"
+              >
+                <span class="name system">系统通知:</span>
+                <span class="msg">
+                  <span>{{ item.userInfo?.username || item.socket_id }}</span>
+                  <span>离开直播!</span>
+                </span>
+              </template>
+            </div>
+          </div>
+        </div>
+        <div class="send-msg">
+          <input
+            v-model="danmuStr"
+            class="ipt"
+            @keydown="keydownDanmu"
+          />
+          <n-button
+            type="info"
+            size="small"
+            @click="sendDanmu"
+          >
+            发送
+          </n-button>
+        </div>
+      </div>
+    </div>
+
+    <SelectMediaModalCpt
+      v-if="showSelectMediaModalCpt"
+      :all-media-type-list="allMediaTypeList"
+      @close="showSelectMediaModalCpt = false"
+      @ok="selectMediaOk"
+    ></SelectMediaModalCpt>
+
+    <MediaModalCpt
+      v-if="showMediaModalCpt"
+      :media-type="currentMediaType"
+      @close="showMediaModalCpt = false"
+      @ok="addMediaOk"
+    ></MediaModalCpt>
+    <OpenMicophoneTipCpt
+      v-if="showOpenMicophoneTipCpt"
+      @close="showOpenMicophoneTipCpt = false"
+    ></OpenMicophoneTipCpt>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import {
+  Close,
+  CreateOutline,
+  VolumeHighOutline,
+  VolumeMuteOutline,
+} from '@vicons/ionicons5';
+import { fabric } from 'fabric';
+import { UploadFileInfo } from 'naive-ui';
+import { NODE_ENV } from 'script/constant';
+import { markRaw, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
+import { useRoute } from 'vue-router';
+import * as workerTimers from 'worker-timers';
+
+import { usePush } from '@/hooks/use-push';
+import { DanmuMsgTypeEnum, MediaTypeEnum, liveTypeEnum } from '@/interface';
+import { AppRootState, useAppStore } from '@/store/app';
+import { useResourceCacheStore } from '@/store/cache';
+import { useUserStore } from '@/store/user';
+import { createVideo, generateBase64, getRandomEnglishString } from '@/utils';
+
+import MediaModalCpt from './mediaModal/index.vue';
+import OpenMicophoneTipCpt from './openMicophoneTip/index.vue';
+import SelectMediaModalCpt from './selectMediaModal/index.vue';
+
+const route = useRoute();
+const userStore = useUserStore();
+const appStore = useAppStore();
+const resourceCacheStore = useResourceCacheStore();
+const currentMediaType = ref(MediaTypeEnum.camera);
+const showOpenMicophoneTipCpt = ref(false);
+const showSelectMediaModalCpt = ref(false);
+const showMediaModalCpt = ref(false);
+const topRef = ref<HTMLDivElement>();
+const bottomRef = ref<HTMLDivElement>();
+const danmuListRef = ref<HTMLDivElement>();
+const containerRef = ref<HTMLDivElement>();
+const pushCanvasRef = ref<HTMLCanvasElement>();
+const fabricCanvas = ref<fabric.Canvas>();
+const localVideoRef = ref<HTMLVideoElement>();
+const audioCtx = ref<AudioContext>();
+const remoteVideoRef = ref<HTMLVideoElement[]>([]);
+const isSRS = route.query.liveType === liveTypeEnum.srsPush;
+const wrapSize = reactive({
+  width: 0,
+  height: 0,
+});
+
+const scaleRatio = ref(0);
+const timerId = ref(-1);
+const videoRatio = ref(16 / 9);
+const {
+  confirmRoomName,
+  getSocketId,
+  startLive,
+  endLive,
+  sendDanmu,
+  keydownDanmu,
+  lastCoverImg,
+  canvasVideoStream,
+  isLiving,
+  allMediaTypeList,
+  currentResolutionRatio,
+  currentMaxBitrate,
+  currentMaxFramerate,
+  resolutionRatio,
+  maxBitrate,
+  maxFramerate,
+  danmuStr,
+  roomName,
+  damuList,
+  liveUserList,
+  addTrack,
+  delTrack,
+} = usePush({
+  localVideoRef,
+  remoteVideoRef,
+  isSRS,
+});
+
+const mediaTypeEnumMap = {
+  [MediaTypeEnum.camera]: '摄像头',
+  [MediaTypeEnum.microphone]: '麦克风',
+  [MediaTypeEnum.screen]: '窗口',
+  [MediaTypeEnum.img]: '图片',
+  [MediaTypeEnum.txt]: '文字',
+  [MediaTypeEnum.media]: '视频',
+};
+
+watch(
+  () => damuList.value.length,
+  () => {
+    setTimeout(() => {
+      if (danmuListRef.value) {
+        danmuListRef.value.scrollTop = danmuListRef.value.scrollHeight;
+      }
+    }, 0);
+  }
+);
+
+// 处理页面显示/隐藏
+function onPageVisibility() {
+  // 注意:此属性在Page Visibility Level 2 规范中被描述为“历史” 。考虑改用该Document.visibilityState 属性。
+  // const isHidden = document.hidden;
+
+  if (document.visibilityState === 'hidden') {
+    console.log(new Date().toLocaleString(), '页面隐藏了', timerId.value);
+    if (isLiving.value) {
+      const delay = 1000 / 60; // 16.666666666666668
+      timerId.value = workerTimers.setInterval(() => {
+        fabricCanvas.value?.renderAll();
+      }, delay);
+    }
+  } else {
+    console.log(new Date().toLocaleString(), '页面显示了', timerId.value);
+    if (isLiving.value) {
+      workerTimers.clearInterval(timerId.value);
+    }
+  }
+}
+
+function initUserMedia() {
+  navigator.mediaDevices
+    .getUserMedia({
+      video: true,
+      audio: false,
+    })
+    .then(() => {
+      console.log('初始化获取摄像头成功');
+    })
+    .catch(() => {
+      console.log('初始化获取摄像头失败');
+    })
+    .finally(() => {
+      navigator.mediaDevices
+        .getUserMedia({
+          video: false,
+          audio: true,
+        })
+        .then(() => {
+          console.log('初始化获取麦克风成功');
+        })
+        .catch(() => {
+          console.log('初始化获取麦克风失败');
+          showOpenMicophoneTipCpt.value = true;
+        });
+    });
+}
+
+function renderFrame() {
+  fabricCanvas.value?.renderAll();
+  window.requestAnimationFrame(renderFrame);
+}
+
+// 处理空音频轨
+function initNullAudio() {
+  console.warn('处理空音频轨');
+  // 创建一个AudioContext实例
+  const audioContext = new AudioContext();
+
+  // 创建一个GainNode实例来控制音频的音量
+  const gainNode = audioContext.createGain();
+
+  // 创建一个空的音频缓存
+  const buffer = audioContext.createBuffer(
+    2,
+    audioContext.sampleRate * 3,
+    audioContext.sampleRate
+  );
+
+  // 创建一个用于播放音频的AudioBufferSourceNode
+  const source = audioContext.createBufferSource();
+  source.buffer = buffer;
+
+  // 将源连接到gain node,再连接到输出
+  source.connect(gainNode);
+  gainNode.connect(audioContext.destination);
+  const destination = audioContext.createMediaStreamDestination();
+
+  const audioTrack: AppRootState['allTrack'][0] = {
+    id: getRandomEnglishString(8),
+    audio: 1,
+    video: 2,
+    mediaName: 'webAudio占位',
+    type: MediaTypeEnum.webAudio,
+    track: destination.stream.getAudioTracks()[0],
+    trackid: destination.stream.getAudioTracks()[0].id,
+    stream: destination.stream,
+    streamid: destination.stream.id,
+    hidden: true,
+    muted: false,
+  };
+  const res = [...appStore.allTrack, audioTrack];
+  appStore.setAllTrack(res);
+  const webAudioItem = resourceCacheStore.list.find(
+    (item) => item.type === MediaTypeEnum.webAudio
+  );
+  if (!webAudioItem) {
+    resourceCacheStore.setList([...resourceCacheStore.list, audioTrack]);
+  }
+  const vel = createVideo({});
+  // vel.style.width = `1px`;
+  // vel.style.height = `1px`;
+  vel.style.position = 'fixed';
+  vel.style.bottom = '0';
+  vel.style.right = '0';
+  // vel.style.opacity = '0';
+  // vel.style.pointerEvents = 'none';
+  vel.srcObject = destination.stream;
+  document.body.appendChild(vel);
+}
+
+let streamTmp: MediaStream;
+let vel;
+
+function handleMixedAudio() {
+  const allAudioTrack = appStore.allTrack.filter((item) => item.audio === 1);
+  if (audioCtx.value) {
+    const gainNode = audioCtx.value.createGain();
+    allAudioTrack.forEach((item) => {
+      if (!audioCtx.value || !item.stream) return;
+      const audioInput = audioCtx.value.createMediaStreamSource(item.stream);
+      audioInput.connect(gainNode);
+      console.log('混流', item.stream?.id, item.stream);
+    });
+    if (streamTmp) {
+      const destination = audioCtx.value.createMediaStreamDestination();
+      streamTmp.addTrack(destination.stream.getAudioTracks()[0]);
+      gainNode.connect(destination);
+      const mixedStream = new MediaStream();
+      mixedStream.addTrack(destination.stream.getAudioTracks()[0]);
+      mixedStream.addTrack(canvasVideoStream.value!.getVideoTracks()[0]);
+      canvasVideoStream.value = mixedStream;
+      console.log('替换了21111');
+      return;
+    }
+    const destination = audioCtx.value.createMediaStreamDestination();
+    streamTmp = destination.stream;
+    // @ts-ignore
+    canvasVideoStream.value?.addTrack(destination.stream.getAudioTracks()[0]);
+    gainNode.connect(destination);
+    vel = createVideo({});
+    vel.style.width = `1px`;
+    vel.style.height = `1px`;
+    vel.style.position = 'fixed';
+    vel.style.bottom = '0';
+    vel.style.right = '0';
+    vel.style.opacity = '0';
+    vel.style.pointerEvents = 'none';
+    vel.srcObject = destination.stream;
+    document.body.appendChild(vel);
+  }
+}
+
+function handleStartLive() {
+  if (!audioCtx.value) {
+    audioCtx.value = new AudioContext();
+  }
+  handleMixedAudio();
+  lastCoverImg.value = generateBase64(pushCanvasRef.value!);
+  startLive();
+}
+
+function handleScale({ width, height }: { width: number; height: number }) {
+  const resolutionHeight =
+    currentResolutionRatio.value / window.devicePixelRatio;
+  const resolutionWidth =
+    (currentResolutionRatio.value / window.devicePixelRatio) * videoRatio.value;
+  console.log('当前分辨率', { resolutionWidth, resolutionHeight });
+  let ratio = 1;
+  if (width > resolutionWidth) {
+    const r1 = resolutionWidth / width;
+    ratio = r1;
+  }
+  if (height > resolutionHeight) {
+    const r1 = resolutionHeight / height;
+    if (ratio > r1) {
+      ratio = r1;
+    }
+  }
+  // if (width > wrapSize.width) {
+  //   const r1 = wrapSize.width / width;
+  //   ratio = r1;
+  // }
+  // if (height > wrapSize.height) {
+  //   const r1 = wrapSize.height / height;
+  //   if (ratio > r1) {
+  //     ratio = r1;
+  //   }
+  // }
+
+  return ratio;
+}
+
+function autoCreateVideo({
+  stream,
+  id,
+  rect,
+  muted,
+}: {
+  stream: MediaStream;
+  id: string;
+  rect?: { left: number; top: number };
+  muted?: boolean;
+}) {
+  console.warn('autoCreateVideo', id);
+  const videoEl = createVideo({});
+  if (muted !== undefined) {
+    videoEl.muted = muted;
+  }
+  videoEl.srcObject = stream;
+  videoEl.style.width = `1px`;
+  videoEl.style.height = `1px`;
+  videoEl.style.position = 'fixed';
+  videoEl.style.bottom = '0';
+  videoEl.style.right = '0';
+  videoEl.style.opacity = '0';
+  videoEl.style.pointerEvents = 'none';
+  document.body.appendChild(videoEl);
+  return new Promise<{
+    canvasDom: fabric.Image;
+    videoEl: HTMLVideoElement;
+    clearFrame: any;
+    scale: number;
+  }>((resolve) => {
+    console.log(videoEl, 888211888888);
+    videoEl.onloadedmetadata = () => {
+      console.log('kkkkkkk2kkk', 123);
+      const width = stream.getVideoTracks()[0].getSettings().width!;
+      const height = stream.getVideoTracks()[0].getSettings().height!;
+      const ratio = handleScale({ width, height });
+      videoEl.width = width;
+      videoEl.height = height;
+
+      const canvasDom = markRaw(
+        new fabric.Image(videoEl, {
+          top: rect?.top || 0,
+          left: rect?.left || 0,
+          width,
+          height,
+        })
+      );
+      console.log(
+        '初始化',
+        ratio,
+        canvasDom.width,
+        canvasDom.height,
+        canvasDom
+      );
+      canvasDom.on('moving', () => {
+        appStore.allTrack.forEach((item) => {
+          if (id === item.id) {
+            item.rect = { top: canvasDom.top!, left: canvasDom.left! };
+          }
+        });
+        resourceCacheStore.setList(appStore.allTrack);
+      });
+
+      canvasDom.scale(ratio);
+      fabricCanvas.value!.add(canvasDom);
+      let timer;
+
+      function clearFrame() {
+        window.cancelAnimationFrame(timer);
+      }
+
+      renderFrame();
+
+      resolve({ canvasDom, scale: ratio, videoEl, clearFrame });
+    };
+  });
+}
+
+watch(
+  () => currentResolutionRatio.value,
+  (newHeight, oldHeight) => {
+    changeCanvasAttr({ newHeight, oldHeight });
+  }
+);
+
+// 容器宽高,1280*720,即720p
+// canvas容器宽高,2560*1440,即1440p
+
+// ======
+// 容器宽高,960*540,即540p
+// dom宽高,640*480
+// canvas容器宽高,960*540,即540p
+// 将dom绘制到容器里,此时dom的大小就是640*480
+// 需求,不管切换多少分辨率,我要看到的dom都是一样大小,即
+// 960*540时,dom是640*480
+// 1280*720时,dom不能是640*480了,因为这样他就会对比上一个分辨率的dom看起来小了,960/1280=0.75,540/720=0.75,
+// 其实就是分辨率变大了,我们就要将图片也变大,即图片的宽是640/0.75=853.4,高是480/0.75=640
+// 坐标变化,960*540时,dom坐标是100,100
+// 1280*720时,dom的坐标不能再是100,100了,否则对比上一个分辨率看起来偏
+
+function changeCanvasAttr({
+  newHeight,
+  oldHeight,
+}: {
+  newHeight: number;
+  oldHeight: number;
+}) {
+  if (fabricCanvas.value) {
+    const resolutionHeight =
+      currentResolutionRatio.value / window.devicePixelRatio;
+    const resolutionWidth =
+      (currentResolutionRatio.value / window.devicePixelRatio) *
+      videoRatio.value;
+    fabricCanvas.value.setWidth(resolutionWidth);
+    fabricCanvas.value.setHeight(resolutionHeight);
+    // fabricCanvas.value.forEachObject((canvas) => {
+    //   canvas.setCoords();
+    // });
+    appStore.allTrack.forEach((item) => {
+      if (item.canvasDom) {
+        // 分辨率变小了,将图片变小
+        if (newHeight < oldHeight) {
+          const ratio = newHeight / oldHeight;
+          const ratio1 = (item.canvasDom.scaleX || 1) * ratio;
+          const ratio2 = oldHeight / newHeight;
+          console.log(
+            ratio,
+            ratio1,
+            '分辨率变小了,将图片变小-----',
+            item.canvasDom
+          );
+          item.canvasDom.scale(ratio1);
+          item.canvasDom.left = item.canvasDom.left! / ratio2;
+          item.canvasDom.top = item.canvasDom.top! / ratio2;
+        } else {
+          // 分辨率变大了,将图片变大
+          const ratio = newHeight / oldHeight;
+          const ratio1 = (item.canvasDom.scaleX || 1) * ratio;
+          const ratio2 = oldHeight / newHeight;
+          console.log(
+            ratio,
+            ratio1,
+            '分辨率变大了,将图片变大-----',
+            item.canvasDom
+          );
+          item.canvasDom.scale(ratio1);
+          item.canvasDom.left = item.canvasDom.left! / ratio2;
+          item.canvasDom.top = item.canvasDom.top! / ratio2;
+        }
+      }
+    });
+    changeCanvasStyle();
+  }
+}
+
+function changeCanvasStyle() {
+  // @ts-ignore
+  fabricCanvas.value.wrapperEl.style.width = `${wrapSize.width}px`;
+  // @ts-ignore
+  fabricCanvas.value.wrapperEl.style.height = `${wrapSize.height}px`;
+  // @ts-ignore
+  fabricCanvas.value.lowerCanvasEl.style.width = `${wrapSize.width}px`;
+  // @ts-ignore
+  fabricCanvas.value.lowerCanvasEl.style.height = `${wrapSize.height}px`;
+  // @ts-ignore
+  fabricCanvas.value.upperCanvasEl.style.width = `${wrapSize.width}px`;
+  // @ts-ignore
+  fabricCanvas.value.upperCanvasEl.style.height = `${wrapSize.height}px`;
+}
+
+function initCanvas() {
+  const resolutionHeight =
+    currentResolutionRatio.value / window.devicePixelRatio;
+  const resolutionWidth =
+    (currentResolutionRatio.value / window.devicePixelRatio) * videoRatio.value;
+  const wrapWidth = containerRef.value!.getBoundingClientRect().width;
+  // const wrapWidth = 1920;
+  const ratio = wrapWidth / resolutionWidth;
+  scaleRatio.value = resolutionWidth / wrapWidth;
+  const wrapHeight = resolutionHeight * ratio;
+  // const wrapHeight = 1080;
+  // lower-canvas: 实际的canvas画面,也就是pushCanvasRef
+  // upper-canvas: 操作时候的canvas
+  const ins = markRaw(new fabric.Canvas(pushCanvasRef.value!));
+  ins.setWidth(resolutionWidth);
+  ins.setHeight(resolutionHeight);
+  ins.setBackgroundColor('black', () => {
+    console.log('setBackgroundColor回调');
+  });
+  wrapSize.width = wrapWidth;
+  wrapSize.height = wrapHeight;
+  fabricCanvas.value = ins;
+  changeCanvasStyle();
+}
+
+async function handleCache() {
+  const res: AppRootState['allTrack'] = [];
+  const queue: any[] = [];
+  resourceCacheStore.list.forEach((item) => {
+    // @ts-ignore
+    const obj: AppRootState['allTrack'][0] = {};
+    obj.audio = item.audio;
+    obj.video = item.video;
+    obj.id = item.id;
+    obj.type = item.type;
+    obj.hidden = item.hidden;
+    obj.mediaName = item.mediaName;
+    obj.muted = item.muted;
+    obj.rect = item.rect;
+    obj.scaleInfo = item.scaleInfo;
+
+    async function handleMediaVideo() {
+      const file = await readFile(item.id);
+      const url = URL.createObjectURL(file);
+      console.log(file, file.name, url);
+      const videoEl = createVideo({});
+      videoEl.src = url;
+      videoEl.muted = item.muted ? item.muted : false;
+      videoEl.style.width = `1px`;
+      videoEl.style.height = `1px`;
+      videoEl.style.position = 'fixed';
+      videoEl.style.bottom = '0';
+      videoEl.style.right = '0';
+      videoEl.style.opacity = '0';
+      videoEl.style.pointerEvents = 'none';
+      document.body.appendChild(videoEl);
+      await new Promise((resolve) => {
+        videoEl.onloadedmetadata = () => {
+          const stream = videoEl
+            // @ts-ignore
+            .captureStream();
+          const width = stream.getVideoTracks()[0].getSettings().width!;
+          const height = stream.getVideoTracks()[0].getSettings().height!;
+          // const ratio = handleScale({ width, height });
+          videoEl.width = width;
+          videoEl.height = height;
+
+          const canvasDom = markRaw(
+            new fabric.Image(videoEl, {
+              top: item.rect?.top || 0,
+              left: item.rect?.left || 0,
+              width,
+              height,
+            })
+          );
+          canvasDom.on('moving', () => {
+            appStore.allTrack.forEach((iten) => {
+              if (item.id === iten.id) {
+                iten.rect = { top: canvasDom.top!, left: canvasDom.left! };
+              }
+            });
+            resourceCacheStore.setList(appStore.allTrack);
+          });
+          canvasDom.scale(item.scaleInfo?.scaleX || 1);
+          fabricCanvas.value!.add(canvasDom);
+
+          renderFrame();
+          obj.videoEl = videoEl;
+          obj.canvasDom = canvasDom;
+          resolve({ videoEl, canvasDom });
+        };
+      });
+      const stream = videoEl
+        // @ts-ignore
+        .captureStream() as MediaStream;
+      obj.stream = stream;
+      obj.streamid = stream.id;
+      obj.track = stream.getVideoTracks()[0];
+      obj.trackid = stream.getVideoTracks()[0].id;
+      // if (stream.getAudioTracks()[0]) {
+      //   console.log('视频有音频', stream.getAudioTracks()[0]);
+      //   mediaVideoTrack.audio = 1;
+      //   const audioTrack: AppRootState['allTrack'][0] = {
+      //     id: mediaVideoTrack.id,
+      //     audio: 1,
+      //     video: 2,
+      //     mediaName: val.mediaName,
+      //     type: MediaTypeEnum.media,
+      //     track: stream.getAudioTracks()[0],
+      //     trackid: stream.getAudioTracks()[0].id,
+      //     stream,
+      //     streamid: stream.id,
+      //     hidden: true,
+      //     muted: false,
+      //   };
+      //   // @ts-ignore
+      //   const res = [...appStore.allTrack, audioTrack];
+      //   appStore.setAllTrack(res);
+      //   resourceCacheStore.setList(res);
+      //   handleMixedAudio();
+      //   // @ts-ignore
+
+      //   addTrack(audioTrack);
+      // }
+    }
+
+    async function handleImg() {
+      const file = await readFile(item.id);
+      const imgEl = await new Promise<HTMLImageElement>((resolve) => {
+        const reader = new FileReader();
+        reader.addEventListener(
+          'load',
+          function () {
+            const img = document.createElement('img');
+            img.src = reader.result as string;
+            img.onload = () => {
+              resolve(img);
+            };
+          },
+          false
+        );
+        if (file) {
+          reader.readAsDataURL(file);
+        }
+      });
+      if (fabricCanvas.value) {
+        const canvasDom = markRaw(
+          new fabric.Image(imgEl, {
+            top: item.rect?.top || 0,
+            left: item.rect?.left || 0,
+            width: imgEl.width,
+            height: imgEl.height,
+          })
+        );
+        canvasDom.on('moving', () => {
+          appStore.allTrack.forEach((item) => {
+            if (obj.id === item.id) {
+              item.rect = { top: canvasDom.top!, left: canvasDom.left! };
+            }
+          });
+          resourceCacheStore.setList(appStore.allTrack);
+        });
+        canvasDom.on('scaling', () => {
+          appStore.allTrack.forEach((item) => {
+            if (obj.id === item.id) {
+              item.scaleInfo = {
+                scaleX: canvasDom.scaleX || 1,
+                scalcY: canvasDom.scaleY || 1,
+              };
+            }
+          });
+          resourceCacheStore.setList(appStore.allTrack);
+        });
+        canvasDom.scale(item.scaleInfo?.scaleX || 1);
+        fabricCanvas.value.add(canvasDom);
+        obj.canvasDom = canvasDom;
+        renderFrame();
+      }
+    }
+    if (item.type === MediaTypeEnum.media && item.video === 1) {
+      queue.push(handleMediaVideo());
+    } else if (item.type === MediaTypeEnum.txt) {
+      obj.txtInfo = item.txtInfo;
+      if (fabricCanvas.value) {
+        const canvasDom = markRaw(
+          new fabric.Text(item.txtInfo?.txt || '', {
+            top: item.rect?.top || 0,
+            left: item.rect?.left || 0,
+            fill: item.txtInfo?.color,
+          })
+        );
+        canvasDom.on('moving', () => {
+          appStore.allTrack.forEach((item) => {
+            if (obj.id === item.id) {
+              item.rect = { top: canvasDom.top!, left: canvasDom.left! };
+            }
+          });
+          resourceCacheStore.setList(appStore.allTrack);
+        });
+        canvasDom.scale(item.scaleInfo?.scaleX || 1);
+        fabricCanvas.value.add(canvasDom);
+        obj.canvasDom = canvasDom;
+        renderFrame();
+      }
+    } else if (item.type === MediaTypeEnum.img) {
+      queue.push(handleImg());
+    }
+    res.push(obj);
+  });
+  await Promise.all(queue);
+  canvasVideoStream.value = pushCanvasRef.value!.captureStream();
+  appStore.setAllTrack(res);
+}
+
+onMounted(() => {
+  setTimeout(() => {
+    scrollTo(0, 0);
+  }, 100);
+  // initNullAudio();
+  initUserMedia();
+  initCanvas();
+  handleCache();
+  document.addEventListener('visibilitychange', onPageVisibility);
+});
+
+onUnmounted(() => {
+  document.removeEventListener('visibilitychange', onPageVisibility);
+});
+
+function selectMediaOk(val: MediaTypeEnum) {
+  showMediaModalCpt.value = true;
+  showSelectMediaModalCpt.value = false;
+  currentMediaType.value = val;
+}
+
+async function addMediaOk(val: {
+  type: MediaTypeEnum;
+  deviceId: string;
+  mediaName: string;
+  txtInfo?: { txt: string; color: string };
+  imgInfo?: UploadFileInfo[];
+  mediaInfo?: UploadFileInfo[];
+}) {
+  if (!audioCtx.value) {
+    audioCtx.value = new AudioContext();
+  }
+  showMediaModalCpt.value = false;
+  if (val.type === MediaTypeEnum.screen) {
+    const event = await navigator.mediaDevices.getDisplayMedia({
+      video: {
+        deviceId: val.deviceId,
+        // displaySurface: 'monitor', // browser默认标签页;window默认窗口;monitor默认整个屏幕
+      },
+      audio: true,
+    });
+
+    const videoTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.screen,
+      track: event.getVideoTracks()[0],
+      trackid: event.getVideoTracks()[0].id,
+      stream: event,
+      streamid: event.id,
+      hidden: false,
+      muted: false,
+    };
+
+    const { canvasDom, videoEl } = await autoCreateVideo({
+      stream: event,
+      id: videoTrack.id,
+    });
+    videoTrack.videoEl = videoEl;
+    // @ts-ignore
+    videoTrack.canvasDom = canvasDom;
+
+    const audio = event.getAudioTracks();
+    if (audio.length) {
+      videoTrack.audio = 1;
+      const audioTrack: AppRootState['allTrack'][0] = {
+        id: videoTrack.id,
+        audio: 1,
+        video: 2,
+        mediaName: val.mediaName,
+        type: MediaTypeEnum.screen,
+        track: event.getAudioTracks()[0],
+        trackid: event.getAudioTracks()[0].id,
+        stream: event,
+        streamid: event.id,
+        hidden: true,
+        muted: false,
+      };
+      const res = [...appStore.allTrack, videoTrack, audioTrack];
+      appStore.setAllTrack(res);
+      resourceCacheStore.setList(res);
+      handleMixedAudio();
+      // @ts-ignore
+      addTrack(videoTrack);
+      // @ts-ignore
+      addTrack(audioTrack);
+    } else {
+      const res = [...appStore.allTrack, videoTrack];
+      appStore.setAllTrack(res);
+      resourceCacheStore.setList(res);
+      // @ts-ignore
+      addTrack(videoTrack);
+    }
+
+    console.log('获取窗口成功');
+  } else if (val.type === MediaTypeEnum.camera) {
+    const event = await navigator.mediaDevices.getUserMedia({
+      video: {
+        deviceId: val.deviceId,
+      },
+      audio: false,
+    });
+    const videoTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.camera,
+      track: event.getVideoTracks()[0],
+      trackid: event.getVideoTracks()[0].id,
+      stream: event,
+      streamid: event.id,
+      hidden: false,
+      muted: false,
+    };
+    const { canvasDom, videoEl } = await autoCreateVideo({
+      stream: event,
+      id: videoTrack.id,
+    });
+    videoTrack.videoEl = videoEl;
+    // @ts-ignore
+    videoTrack.canvasDom = canvasDom;
+
+    const res = [...appStore.allTrack, videoTrack];
+    appStore.setAllTrack(res);
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(videoTrack);
+    console.log('获取摄像头成功');
+  } else if (val.type === MediaTypeEnum.microphone) {
+    const event = await navigator.mediaDevices.getUserMedia({
+      video: false,
+      audio: { deviceId: val.deviceId },
+    });
+    const audioTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 1,
+      video: 2,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.microphone,
+      track: event.getAudioTracks()[0],
+      trackid: event.getAudioTracks()[0].id,
+      stream: event,
+      streamid: event.id,
+      hidden: false,
+      muted: false,
+    };
+    const res = [...appStore.allTrack, audioTrack];
+    appStore.setAllTrack(res);
+    resourceCacheStore.setList(res);
+    handleMixedAudio();
+    // @ts-ignore
+    addTrack(audioTrack);
+
+    console.log('获取麦克风成功');
+  } else if (val.type === MediaTypeEnum.txt) {
+    const txtTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.txt,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+    };
+    if (fabricCanvas.value) {
+      const canvasDom = markRaw(
+        new fabric.Text(val.txtInfo?.txt || '', {
+          top: 0,
+          left: 0,
+          fill: val.txtInfo?.color,
+        })
+      );
+      canvasDom.on('moving', () => {
+        appStore.allTrack.forEach((item) => {
+          if (txtTrack.id === item.id) {
+            item.rect = { top: canvasDom.top!, left: canvasDom.left! };
+          }
+        });
+        resourceCacheStore.setList(appStore.allTrack);
+      });
+      txtTrack.txtInfo = val.txtInfo;
+      // @ts-ignore
+      txtTrack.canvasDom = canvasDom;
+      fabricCanvas.value.add(canvasDom);
+      renderFrame();
+    }
+
+    const res = [...appStore.allTrack, txtTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(txtTrack);
+
+    console.log('获取文字成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.img) {
+    const imgTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.img,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+    };
+
+    if (fabricCanvas.value) {
+      const imgEl = await new Promise<HTMLImageElement>((resolve) => {
+        if (!val.imgInfo) return;
+        const file = val.imgInfo[0].file!;
+        saveFile({ file, fileName: imgTrack.id });
+        const reader = new FileReader();
+        reader.addEventListener(
+          'load',
+          function () {
+            const img = document.createElement('img');
+            img.src = reader.result as string;
+            img.onload = () => {
+              resolve(img);
+            };
+          },
+          false
+        );
+        if (file) {
+          reader.readAsDataURL(file);
+        }
+      });
+
+      const canvasDom = markRaw(
+        new fabric.Image(imgEl, {
+          top: 0,
+          left: 0,
+          width: imgEl.width,
+          height: imgEl.height,
+        })
+      );
+      canvasDom.on('moving', () => {
+        appStore.allTrack.forEach((item) => {
+          if (imgTrack.id === item.id) {
+            item.rect = { top: canvasDom.top!, left: canvasDom.left! };
+          }
+        });
+        resourceCacheStore.setList(appStore.allTrack);
+      });
+      const ratio = handleScale({ width: imgEl.width, height: imgEl.height });
+      // @ts-ignore
+      imgTrack.canvasDom = canvasDom;
+      imgTrack.scaleInfo = { scaleX: ratio, scalcY: ratio };
+      canvasDom.scale(ratio);
+      fabricCanvas.value.add(canvasDom);
+      renderFrame();
+    }
+
+    const res = [...appStore.allTrack, imgTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(imgTrack);
+
+    console.log('获取图片成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.media) {
+    const mediaVideoTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.media,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+    };
+    if (fabricCanvas.value) {
+      if (!val.mediaInfo) return;
+      const file = val.mediaInfo[0].file!;
+      saveFile({ file, fileName: mediaVideoTrack.id });
+      const url = URL.createObjectURL(file);
+      console.log(file, file.name, url);
+      const videoEl = createVideo({});
+      videoEl.src = url;
+      videoEl.muted = false;
+      videoEl.style.width = `1px`;
+      videoEl.style.height = `1px`;
+      videoEl.style.position = 'fixed';
+      videoEl.style.bottom = '0';
+      videoEl.style.right = '0';
+      videoEl.style.opacity = '0';
+      videoEl.style.pointerEvents = 'none';
+      document.body.appendChild(videoEl);
+      const videoRes = await new Promise<HTMLVideoElement>((resolve) => {
+        videoEl.onloadedmetadata = () => {
+          resolve(videoEl);
+        };
+      });
+      // @ts-ignore
+      const stream = videoRes.captureStream();
+      const { canvasDom } = await autoCreateVideo({
+        stream,
+        id: mediaVideoTrack.id,
+      });
+      mediaVideoTrack.videoEl = videoEl;
+      // @ts-ignore
+      mediaVideoTrack.canvasDom = canvasDom;
+      if (stream.getAudioTracks()[0]) {
+        console.log('视频有音频', stream.getAudioTracks()[0]);
+        mediaVideoTrack.audio = 1;
+        const audioTrack: AppRootState['allTrack'][0] = {
+          id: mediaVideoTrack.id,
+          audio: 1,
+          video: 2,
+          mediaName: val.mediaName,
+          type: MediaTypeEnum.media,
+          track: stream.getAudioTracks()[0],
+          trackid: stream.getAudioTracks()[0].id,
+          stream,
+          streamid: stream.id,
+          hidden: true,
+          muted: false,
+        };
+        // @ts-ignore
+        const res = [...appStore.allTrack, audioTrack];
+        appStore.setAllTrack(res);
+        resourceCacheStore.setList(res);
+        handleMixedAudio();
+        // @ts-ignore
+
+        addTrack(audioTrack);
+      }
+    }
+    const res = [...appStore.allTrack, mediaVideoTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+
+    addTrack(mediaVideoTrack);
+
+    console.log('获取视频成功', fabricCanvas.value);
+  }
+
+  canvasVideoStream.value = pushCanvasRef.value!.captureStream();
+}
+
+function handleChangeMuted(item: AppRootState['allTrack'][0]) {
+  console.log('handleChangeMuted', item);
+  if (item.videoEl) {
+    const res = !item.videoEl.muted;
+    item.videoEl.muted = res;
+    item.muted = res;
+    resourceCacheStore.setList(appStore.allTrack);
+  }
+}
+
+function handleEdit(item: AppRootState['allTrack'][0]) {
+  console.log('handleEdit', item);
+}
+
+function handleDel(item: AppRootState['allTrack'][0]) {
+  console.log('handleDel', item);
+  if (item.canvasDom !== undefined) {
+    // @ts-ignore
+    fabricCanvas.value?.remove(item.canvasDom);
+    item.videoEl?.remove();
+  }
+  const res = appStore.allTrack.filter((iten) => iten.id !== item.id);
+  appStore.setAllTrack(res);
+  resourceCacheStore.setList(res);
+  delTrack(item);
+}
+
+function saveFile(data: { file: File; fileName: string }) {
+  const { file, fileName } = data;
+  const requestFileSystem =
+    // @ts-ignore
+    window.requestFileSystem || window.webkitRequestFileSystem;
+  function onError(err) {
+    console.error('saveFile错误', data.fileName);
+    console.log(err);
+  }
+  function onFs(fs) {
+    // 创建文件
+    fs.root.getFile(
+      fileName,
+      { create: true },
+      function (fileEntry) {
+        // 创建文件写入流
+        fileEntry.createWriter(function (fileWriter) {
+          fileWriter.onwriteend = () => {
+            // 完成后关闭文件
+            fileWriter.abort();
+          };
+          // 写入文件内容
+          fileWriter.write(file);
+        });
+      },
+      () => {
+        console.log('写入文件失败');
+      }
+    );
+  }
+  // Opening a file system with temporary storage
+  requestFileSystem(
+    // @ts-ignore
+    window.PERSISTENT,
+    0,
+    onFs,
+    onError
+  );
+}
+
+function readFile(fileName: string) {
+  return new Promise<File>((resolve) => {
+    const requestFileSystem =
+      // @ts-ignore
+      window.requestFileSystem || window.webkitRequestFileSystem;
+    function onError(err) {
+      console.error('readFile错误', fileName);
+      console.log(err);
+    }
+    function onFs(fs) {
+      fs.root.getFile(
+        fileName,
+        {},
+        function (fileEntry) {
+          fileEntry.file(function (file) {
+            resolve(file);
+            // const url = URL.createObjectURL(file);
+            // const videoEl = createVideo({});
+            // videoEl.src = url;
+            // document.body.appendChild(videoEl);
+          }, onError);
+        },
+        onError
+      );
+    }
+    // Opening a file system with temporary storage
+    requestFileSystem(
+      // @ts-ignore
+      window.PERSISTENT,
+      0,
+      onFs,
+      onError
+    );
+  });
+}
+
+function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
+  currentMediaType.value = item.type;
+  showMediaModalCpt.value = true;
+}
+</script>
+
+<style lang="scss" scoped>
+.push-wrap {
+  display: flex;
+  justify-content: space-between;
+  margin: 15px auto 0;
+  width: $w-1250;
+  .left {
+    position: relative;
+    display: inline-block;
+    overflow: hidden;
+    box-sizing: border-box;
+    width: $w-960;
+    height: 100%;
+    border-radius: 6px;
+    background-color: white;
+    color: #9499a0;
+    vertical-align: top;
+
+    .container {
+      position: relative;
+      overflow: hidden;
+      height: 100%;
+      background-color: rgba($color: #000000, $alpha: 0.5);
+      line-height: 0;
+
+      :deep(canvas) {
+        // width: 100%;
+      }
+
+      .add-wrap {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: space-around;
+        padding: 0 20px;
+        height: 50px;
+        border-radius: 6px;
+        background-color: white;
+        transform: translate(-50%, -50%);
+      }
+    }
+    .room-control {
+      display: flex;
+      justify-content: space-between;
+      padding: 20px;
+      background-color: papayawhip;
+
+      .info {
+        display: flex;
+        align-items: center;
+
+        .avatar {
+          margin-right: 20px;
+          width: 55px;
+          height: 55px;
+          border-radius: 50%;
+          background-position: center center;
+          background-size: cover;
+          background-repeat: no-repeat;
+        }
+        .detail {
+          display: flex;
+          flex-direction: column;
+          flex-shrink: 0;
+          width: 200px;
+          text-align: initial;
+          .top {
+            margin-bottom: 10px;
+            color: #18191c;
+          }
+          .bottom {
+            font-size: 14px;
+          }
+        }
+      }
+      .rtc {
+        display: flex;
+        align-items: center;
+        flex: 1;
+        font-size: 14px;
+        .item {
+          display: flex;
+          align-items: center;
+          flex: 1;
+          .txt {
+            flex-shrink: 0;
+            width: 80px;
+          }
+          .down {
+            width: 90px;
+
+            user-select: none;
+          }
+        }
+      }
+      .other {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        font-size: 12px;
+        .top {
+        }
+        .bottom {
+          margin-top: 10px;
+        }
+      }
+    }
+  }
+  .right {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    box-sizing: border-box;
+    margin-left: 10px;
+    width: $w-250;
+    border-radius: 6px;
+    background-color: white;
+    color: #9499a0;
+
+    .resource-card {
+      position: relative;
+      box-sizing: border-box;
+      margin-bottom: 10px;
+      padding: 10px;
+      width: 100%;
+      height: 290px;
+      border-radius: 6px;
+      background-color: papayawhip;
+      .title {
+        text-align: initial;
+      }
+      .item {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        margin: 5px 0;
+        font-size: 14px;
+        // &:hover {
+        //   .control {
+        //     display: flex;
+        //     align-items: center;
+        //   }
+        // }
+        .control {
+          display: flex;
+          align-items: center;
+          .control-item {
+            cursor: pointer;
+            &:not(:last-child) {
+              margin-right: 6px;
+            }
+          }
+        }
+      }
+      .bottom {
+        position: absolute;
+        bottom: 0;
+        left: 0;
+        padding: 10px;
+      }
+    }
+    .danmu-card {
+      position: relative;
+      flex: 1;
+      box-sizing: border-box;
+      padding: 10px;
+      width: 100%;
+      border-radius: 6px;
+      background-color: papayawhip;
+      text-align: initial;
+      .title {
+        margin-bottom: 10px;
+      }
+      .list {
+        overflow: scroll;
+        height: 360px;
+
+        @extend %hideScrollbar;
+
+        .item {
+          margin-bottom: 10px;
+          font-size: 12px;
+          .name {
+            color: #9499a0;
+          }
+          .msg {
+            color: #61666d;
+          }
+        }
+      }
+
+      .send-msg {
+        position: absolute;
+        bottom: 10px;
+        left: 50%;
+        display: flex;
+        align-items: center;
+        box-sizing: border-box;
+        width: calc(100% - 20px);
+        transform: translateX(-50%);
+        .ipt {
+          display: block;
+          box-sizing: border-box;
+          margin: 0 auto;
+          margin-right: 10px;
+          padding: 10px;
+          width: 80%;
+          height: 30px;
+          outline: none;
+          border: 1px solid hsla(0, 0%, 60%, 0.2);
+          border-radius: 4px;
+          background-color: #f1f2f3;
+          font-size: 14px;
+        }
+      }
+    }
+  }
+}
+
+// 屏幕宽度大于1500的时候
+@media screen and (min-width: $w-1500) {
+  .push-wrap {
+    width: $w-1475;
+    .left {
+      width: $w-1150;
+    }
+    .right {
+      width: $w-300;
+    }
+  }
+}
+</style>

+ 1639 - 0
src/views/pushByCanvas/index3.vue

@@ -0,0 +1,1639 @@
+<template>
+  <div class="push-wrap">
+    <div
+      ref="topRef"
+      class="left"
+    >
+      <div
+        ref="containerRef"
+        class="container"
+      >
+        <canvas
+          id="pushCanvasRef"
+          ref="pushCanvasRef"
+        ></canvas>
+        <div
+          v-if="appStore.allTrack.filter((item) => !item.hidden).length <= 0"
+          class="add-wrap"
+        >
+          <n-space>
+            <n-button
+              v-for="(item, index) in allMediaTypeList"
+              :key="index"
+              class="item"
+              @click="handleStartMedia(item)"
+            >
+              {{ item.txt }}
+            </n-button>
+          </n-space>
+        </div>
+      </div>
+
+      <div
+        ref="bottomRef"
+        class="room-control"
+      >
+        <div class="info">
+          <div
+            class="avatar"
+            :style="{ backgroundImage: `url(${userStore.userInfo?.avatar})` }"
+          ></div>
+          <div class="detail">
+            <div class="top">
+              <n-input-group>
+                <n-input
+                  v-model:value="roomName"
+                  size="small"
+                  placeholder="输入房间名"
+                  :style="{ width: '50%' }"
+                />
+                <n-button
+                  size="small"
+                  type="primary"
+                  @click="confirmRoomName"
+                >
+                  确定
+                </n-button>
+              </n-input-group>
+            </div>
+            <div class="bottom">
+              <span v-if="NODE_ENV === 'development'">
+                {{ getSocketId() }}
+              </span>
+            </div>
+          </div>
+        </div>
+        <div class="rtc">
+          <div class="item">
+            <div class="txt">码率设置</div>
+            <div class="down">
+              <n-select
+                v-model:value="currentMaxBitrate"
+                :options="maxBitrate"
+              />
+            </div>
+          </div>
+          <div class="item">
+            <div class="txt">帧率设置</div>
+            <div class="down">
+              <n-select
+                v-model:value="currentMaxFramerate"
+                :options="maxFramerate"
+              />
+            </div>
+          </div>
+          <div class="item">
+            <div class="txt">分辨率设置</div>
+            <div class="down">
+              <n-select
+                v-model:value="currentResolutionRatio"
+                :options="resolutionRatio"
+              />
+            </div>
+          </div>
+        </div>
+        <div class="other">
+          <div class="top">
+            <span class="item">
+              <i class="ico"></i>
+              <span>
+                正在观看:
+                {{
+                  liveUserList.filter((item) => item.id !== getSocketId())
+                    .length
+                }}
+              </span>
+            </span>
+          </div>
+          <div class="bottom">
+            <n-button
+              v-if="!isLiving"
+              type="info"
+              size="small"
+              @click="handleStartLive"
+            >
+              开始直播
+            </n-button>
+            <n-button
+              v-else
+              type="error"
+              size="small"
+              @click="endLive"
+            >
+              结束直播
+            </n-button>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="right">
+      <div class="resource-card">
+        <div class="title">素材列表</div>
+        <div class="list">
+          <div
+            v-for="(item, index) in appStore.allTrack.filter(
+              (item) => !item.hidden
+            )"
+            :key="index"
+            class="item"
+          >
+            <span class="name">
+              ({{ mediaTypeEnumMap[item.type] }}){{ item.mediaName }}
+            </span>
+            <div class="control">
+              <div
+                v-if="item.audio === 1"
+                class="control-item"
+                @click="handleChangeMuted(item)"
+              >
+                <n-icon size="16">
+                  <VolumeMuteOutline v-if="item.muted"></VolumeMuteOutline>
+                  <VolumeHighOutline v-else></VolumeHighOutline>
+                </n-icon>
+              </div>
+              <div
+                class="control-item"
+                @click="handleEdit(item)"
+              >
+                <n-icon size="16">
+                  <CreateOutline></CreateOutline>
+                </n-icon>
+              </div>
+              <div
+                class="control-item"
+                @click="handleDel(item)"
+              >
+                <n-icon size="16">
+                  <Close></Close>
+                </n-icon>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="bottom">
+          <n-button
+            size="small"
+            type="primary"
+            @click="showSelectMediaModalCpt = true"
+          >
+            添加素材
+          </n-button>
+        </div>
+      </div>
+      <div class="danmu-card">
+        <div class="title">弹幕互动</div>
+        <div class="list-wrap">
+          <div
+            ref="danmuListRef"
+            class="list"
+          >
+            <div
+              v-for="(item, index) in damuList"
+              :key="index"
+              class="item"
+            >
+              <template v-if="item.msgType === DanmuMsgTypeEnum.danmu">
+                <span class="name">
+                  {{ item.userInfo?.username || item.socket_id }}:
+                </span>
+                <span class="msg">{{ item.msg }}</span>
+              </template>
+              <template v-else-if="item.msgType === DanmuMsgTypeEnum.otherJoin">
+                <span class="name system">系统通知:</span>
+                <span class="msg">
+                  <span>{{ item.userInfo?.username || item.socket_id }}</span>
+                  <span>进入直播!</span>
+                </span>
+              </template>
+              <template
+                v-else-if="item.msgType === DanmuMsgTypeEnum.userLeaved"
+              >
+                <span class="name system">系统通知:</span>
+                <span class="msg">
+                  <span>{{ item.userInfo?.username || item.socket_id }}</span>
+                  <span>离开直播!</span>
+                </span>
+              </template>
+            </div>
+          </div>
+        </div>
+        <div class="send-msg">
+          <input
+            v-model="danmuStr"
+            class="ipt"
+            @keydown="keydownDanmu"
+          />
+          <n-button
+            type="info"
+            size="small"
+            @click="sendDanmu"
+          >
+            发送
+          </n-button>
+        </div>
+      </div>
+    </div>
+
+    <SelectMediaModalCpt
+      v-if="showSelectMediaModalCpt"
+      :all-media-type-list="allMediaTypeList"
+      @close="showSelectMediaModalCpt = false"
+      @ok="selectMediaOk"
+    ></SelectMediaModalCpt>
+
+    <MediaModalCpt
+      v-if="showMediaModalCpt"
+      :media-type="currentMediaType"
+      @close="showMediaModalCpt = false"
+      @ok="addMediaOk"
+    ></MediaModalCpt>
+    <OpenMicophoneTipCpt
+      v-if="showOpenMicophoneTipCpt"
+      @close="showOpenMicophoneTipCpt = false"
+    ></OpenMicophoneTipCpt>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import {
+  Close,
+  CreateOutline,
+  VolumeHighOutline,
+  VolumeMuteOutline,
+} from '@vicons/ionicons5';
+import { fabric } from 'fabric';
+import { UploadFileInfo } from 'naive-ui';
+import { NODE_ENV } from 'script/constant';
+import { markRaw, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
+import { useRoute } from 'vue-router';
+import * as workerTimers from 'worker-timers';
+
+import { usePush } from '@/hooks/use-push';
+import { DanmuMsgTypeEnum, MediaTypeEnum, liveTypeEnum } from '@/interface';
+import { AppRootState, useAppStore } from '@/store/app';
+import { useResourceCacheStore } from '@/store/cache';
+import { useUserStore } from '@/store/user';
+import { createVideo, generateBase64, getRandomEnglishString } from '@/utils';
+
+import MediaModalCpt from './mediaModal/index.vue';
+import OpenMicophoneTipCpt from './openMicophoneTip/index.vue';
+import SelectMediaModalCpt from './selectMediaModal/index.vue';
+
+const route = useRoute();
+const userStore = useUserStore();
+const appStore = useAppStore();
+const resourceCacheStore = useResourceCacheStore();
+const currentMediaType = ref(MediaTypeEnum.camera);
+const showOpenMicophoneTipCpt = ref(false);
+const showSelectMediaModalCpt = ref(false);
+const showMediaModalCpt = ref(false);
+const topRef = ref<HTMLDivElement>();
+const bottomRef = ref<HTMLDivElement>();
+const danmuListRef = ref<HTMLDivElement>();
+const containerRef = ref<HTMLDivElement>();
+const pushCanvasRef = ref<HTMLCanvasElement>();
+const fabricCanvas = ref<fabric.Canvas>();
+const localVideoRef = ref<HTMLVideoElement>();
+const audioCtx = ref<AudioContext>();
+const remoteVideoRef = ref<HTMLVideoElement[]>([]);
+const isSRS = route.query.liveType === liveTypeEnum.srsPush;
+const wrapSize = reactive({
+  width: 0,
+  height: 0,
+});
+
+const scaleRatio = ref(0);
+const timerId = ref(-1);
+const videoRatio = ref(16 / 9);
+const {
+  confirmRoomName,
+  getSocketId,
+  startLive,
+  endLive,
+  sendDanmu,
+  keydownDanmu,
+  lastCoverImg,
+  canvasVideoStream,
+  isLiving,
+  allMediaTypeList,
+  currentResolutionRatio,
+  currentMaxBitrate,
+  currentMaxFramerate,
+  resolutionRatio,
+  maxBitrate,
+  maxFramerate,
+  danmuStr,
+  roomName,
+  damuList,
+  liveUserList,
+  addTrack,
+  delTrack,
+} = usePush({
+  localVideoRef,
+  remoteVideoRef,
+  isSRS,
+});
+
+const mediaTypeEnumMap = {
+  [MediaTypeEnum.camera]: '摄像头',
+  [MediaTypeEnum.microphone]: '麦克风',
+  [MediaTypeEnum.screen]: '窗口',
+  [MediaTypeEnum.img]: '图片',
+  [MediaTypeEnum.txt]: '文字',
+  [MediaTypeEnum.media]: '视频',
+};
+
+watch(
+  () => damuList.value.length,
+  () => {
+    setTimeout(() => {
+      if (danmuListRef.value) {
+        danmuListRef.value.scrollTop = danmuListRef.value.scrollHeight;
+      }
+    }, 0);
+  }
+);
+
+// 处理页面显示/隐藏
+function onPageVisibility() {
+  // 注意:此属性在Page Visibility Level 2 规范中被描述为“历史” 。考虑改用该Document.visibilityState 属性。
+  // const isHidden = document.hidden;
+
+  if (document.visibilityState === 'hidden') {
+    console.log(new Date().toLocaleString(), '页面隐藏了', timerId.value);
+    if (isLiving.value) {
+      const delay = 1000 / 60; // 16.666666666666668
+      timerId.value = workerTimers.setInterval(() => {
+        fabricCanvas.value?.renderAll();
+      }, delay);
+    }
+  } else {
+    console.log(new Date().toLocaleString(), '页面显示了', timerId.value);
+    if (isLiving.value) {
+      workerTimers.clearInterval(timerId.value);
+    }
+  }
+}
+
+function initUserMedia() {
+  navigator.mediaDevices
+    .getUserMedia({
+      video: true,
+      audio: false,
+    })
+    .then(() => {
+      console.log('初始化获取摄像头成功');
+    })
+    .catch(() => {
+      console.log('初始化获取摄像头失败');
+    })
+    .finally(() => {
+      navigator.mediaDevices
+        .getUserMedia({
+          video: false,
+          audio: true,
+        })
+        .then(() => {
+          console.log('初始化获取麦克风成功');
+        })
+        .catch(() => {
+          console.log('初始化获取麦克风失败');
+          showOpenMicophoneTipCpt.value = true;
+        });
+    });
+}
+
+function renderFrame() {
+  fabricCanvas.value?.renderAll();
+  window.requestAnimationFrame(renderFrame);
+}
+
+// 处理空音频轨
+function initNullAudio() {
+  console.warn('处理空音频轨');
+  // 创建一个AudioContext实例
+  const audioContext = new AudioContext();
+
+  // 创建一个GainNode实例来控制音频的音量
+  const gainNode = audioContext.createGain();
+
+  // 创建一个空的音频缓存
+  const buffer = audioContext.createBuffer(
+    2,
+    audioContext.sampleRate * 3,
+    audioContext.sampleRate
+  );
+
+  // 创建一个用于播放音频的AudioBufferSourceNode
+  const source = audioContext.createBufferSource();
+  source.buffer = buffer;
+
+  // 将源连接到gain node,再连接到输出
+  source.connect(gainNode);
+  gainNode.connect(audioContext.destination);
+  const destination = audioContext.createMediaStreamDestination();
+
+  const audioTrack: AppRootState['allTrack'][0] = {
+    id: getRandomEnglishString(8),
+    audio: 1,
+    video: 2,
+    mediaName: 'webAudio占位',
+    type: MediaTypeEnum.webAudio,
+    track: destination.stream.getAudioTracks()[0],
+    trackid: destination.stream.getAudioTracks()[0].id,
+    stream: destination.stream,
+    streamid: destination.stream.id,
+    hidden: true,
+    muted: false,
+  };
+  const res = [...appStore.allTrack, audioTrack];
+  appStore.setAllTrack(res);
+  const webAudioItem = resourceCacheStore.list.find(
+    (item) => item.type === MediaTypeEnum.webAudio
+  );
+  if (!webAudioItem) {
+    resourceCacheStore.setList([...resourceCacheStore.list, audioTrack]);
+  }
+  const vel = createVideo({});
+  // vel.style.width = `1px`;
+  // vel.style.height = `1px`;
+  vel.style.position = 'fixed';
+  vel.style.bottom = '0';
+  vel.style.right = '0';
+  // vel.style.opacity = '0';
+  // vel.style.pointerEvents = 'none';
+  vel.srcObject = destination.stream;
+  document.body.appendChild(vel);
+}
+
+let streamTmp: MediaStream;
+let vel;
+
+function handleMixedAudio() {
+  const allAudioTrack = appStore.allTrack.filter((item) => item.audio === 1);
+  if (audioCtx.value) {
+    const gainNode = audioCtx.value.createGain();
+    allAudioTrack.forEach((item) => {
+      if (!audioCtx.value || !item.stream) return;
+      const audioInput = audioCtx.value.createMediaStreamSource(item.stream);
+      audioInput.connect(gainNode);
+      console.log('混流', item.stream?.id, item.stream);
+    });
+    if (streamTmp) {
+      const destination = audioCtx.value.createMediaStreamDestination();
+      streamTmp.addTrack(destination.stream.getAudioTracks()[0]);
+      gainNode.connect(destination);
+      const mixedStream = new MediaStream();
+      mixedStream.addTrack(destination.stream.getAudioTracks()[0]);
+      mixedStream.addTrack(canvasVideoStream.value!.getVideoTracks()[0]);
+      canvasVideoStream.value = mixedStream;
+      console.log('替换了21111');
+      return;
+    }
+    const destination = audioCtx.value.createMediaStreamDestination();
+    streamTmp = destination.stream;
+    // @ts-ignore
+    canvasVideoStream.value?.addTrack(destination.stream.getAudioTracks()[0]);
+    gainNode.connect(destination);
+    vel = createVideo({});
+    vel.style.width = `1px`;
+    vel.style.height = `1px`;
+    vel.style.position = 'fixed';
+    vel.style.bottom = '0';
+    vel.style.right = '0';
+    vel.style.opacity = '0';
+    vel.style.pointerEvents = 'none';
+    vel.srcObject = destination.stream;
+    document.body.appendChild(vel);
+  }
+}
+
+function handleStartLive() {
+  if (!audioCtx.value) {
+    audioCtx.value = new AudioContext();
+  }
+  handleMixedAudio();
+  lastCoverImg.value = generateBase64(pushCanvasRef.value!);
+  startLive();
+}
+
+function handleScale({ width, height }: { width: number; height: number }) {
+  const resolutionHeight =
+    currentResolutionRatio.value / window.devicePixelRatio;
+  const resolutionWidth =
+    (currentResolutionRatio.value / window.devicePixelRatio) * videoRatio.value;
+  console.log('当前分辨率', { resolutionWidth, resolutionHeight });
+  let ratio = 1;
+  if (width > resolutionWidth) {
+    const r1 = resolutionWidth / width;
+    ratio = r1;
+  }
+  if (height > resolutionHeight) {
+    const r1 = resolutionHeight / height;
+    if (ratio > r1) {
+      ratio = r1;
+    }
+  }
+  // if (width > wrapSize.width) {
+  //   const r1 = wrapSize.width / width;
+  //   ratio = r1;
+  // }
+  // if (height > wrapSize.height) {
+  //   const r1 = wrapSize.height / height;
+  //   if (ratio > r1) {
+  //     ratio = r1;
+  //   }
+  // }
+
+  return ratio;
+}
+
+function autoCreateVideo({
+  stream,
+  id,
+  rect,
+  muted,
+}: {
+  stream: MediaStream;
+  id: string;
+  rect?: { left: number; top: number };
+  muted?: boolean;
+}) {
+  console.warn('autoCreateVideo', id);
+  const videoEl = createVideo({});
+  if (muted !== undefined) {
+    videoEl.muted = muted;
+  }
+  videoEl.srcObject = stream;
+  videoEl.style.width = `1px`;
+  videoEl.style.height = `1px`;
+  videoEl.style.position = 'fixed';
+  videoEl.style.bottom = '0';
+  videoEl.style.right = '0';
+  videoEl.style.opacity = '0';
+  videoEl.style.pointerEvents = 'none';
+  document.body.appendChild(videoEl);
+  return new Promise<{
+    canvasDom: fabric.Image;
+    videoEl: HTMLVideoElement;
+    clearFrame: any;
+    scale: number;
+  }>((resolve) => {
+    console.log(videoEl, 888211888888);
+    videoEl.onloadedmetadata = () => {
+      console.log('kkkkkkk2kkk', 123);
+      const width = stream.getVideoTracks()[0].getSettings().width!;
+      const height = stream.getVideoTracks()[0].getSettings().height!;
+      const ratio = handleScale({ width, height });
+      videoEl.width = width;
+      videoEl.height = height;
+
+      const canvasDom = markRaw(
+        new fabric.Image(videoEl, {
+          top: rect?.top || 0,
+          left: rect?.left || 0,
+          width,
+          height,
+        })
+      );
+      console.log(
+        '初始化',
+        ratio,
+        canvasDom.width,
+        canvasDom.height,
+        canvasDom
+      );
+      canvasDom.on('moving', () => {
+        appStore.allTrack.forEach((item) => {
+          if (id === item.id) {
+            item.rect = { top: canvasDom.top!, left: canvasDom.left! };
+          }
+        });
+        resourceCacheStore.setList(appStore.allTrack);
+      });
+
+      canvasDom.scale(ratio);
+      fabricCanvas.value!.add(canvasDom);
+      let timer;
+
+      function clearFrame() {
+        window.cancelAnimationFrame(timer);
+      }
+
+      renderFrame();
+
+      resolve({ canvasDom, scale: ratio, videoEl, clearFrame });
+    };
+  });
+}
+
+watch(
+  () => currentResolutionRatio.value,
+  (newHeight, oldHeight) => {
+    changeCanvasAttr({ newHeight, oldHeight });
+  }
+);
+
+// 容器宽高,1280*720,即720p
+// canvas容器宽高,2560*1440,即1440p
+
+// ======
+// 容器宽高,960*540,即540p
+// dom宽高,640*480
+// canvas容器宽高,960*540,即540p
+// 将dom绘制到容器里,此时dom的大小就是640*480
+// 需求,不管切换多少分辨率,我要看到的dom都是一样大小,即
+// 960*540时,dom是640*480
+// 1280*720时,dom不能是640*480了,因为这样他就会对比上一个分辨率的dom看起来小了,960/1280=0.75,540/720=0.75,
+// 其实就是分辨率变大了,我们就要将图片也变大,即图片的宽是640/0.75=853.4,高是480/0.75=640
+// 坐标变化,960*540时,dom坐标是100,100
+// 1280*720时,dom的坐标不能再是100,100了,否则对比上一个分辨率看起来偏
+
+function changeCanvasAttr({
+  newHeight,
+  oldHeight,
+}: {
+  newHeight: number;
+  oldHeight: number;
+}) {
+  if (fabricCanvas.value) {
+    const resolutionHeight =
+      currentResolutionRatio.value / window.devicePixelRatio;
+    const resolutionWidth =
+      (currentResolutionRatio.value / window.devicePixelRatio) *
+      videoRatio.value;
+    fabricCanvas.value.setWidth(resolutionWidth);
+    fabricCanvas.value.setHeight(resolutionHeight);
+    // fabricCanvas.value.forEachObject((canvas) => {
+    //   canvas.setCoords();
+    // });
+    appStore.allTrack.forEach((item) => {
+      if (item.canvasDom) {
+        // 分辨率变小了,将图片变小
+        if (newHeight < oldHeight) {
+          const ratio = newHeight / oldHeight;
+          const ratio1 = (item.canvasDom.scaleX || 1) * ratio;
+          const ratio2 = oldHeight / newHeight;
+          console.log(
+            ratio,
+            ratio1,
+            '分辨率变小了,将图片变小-----',
+            item.canvasDom
+          );
+          item.canvasDom.scale(ratio1);
+          item.canvasDom.left = item.canvasDom.left! / ratio2;
+          item.canvasDom.top = item.canvasDom.top! / ratio2;
+        } else {
+          // 分辨率变大了,将图片变大
+          const ratio = newHeight / oldHeight;
+          const ratio1 = (item.canvasDom.scaleX || 1) * ratio;
+          const ratio2 = oldHeight / newHeight;
+          console.log(
+            ratio,
+            ratio1,
+            '分辨率变大了,将图片变大-----',
+            item.canvasDom
+          );
+          item.canvasDom.scale(ratio1);
+          item.canvasDom.left = item.canvasDom.left! / ratio2;
+          item.canvasDom.top = item.canvasDom.top! / ratio2;
+        }
+      }
+    });
+    changeCanvasStyle();
+  }
+}
+
+function changeCanvasStyle() {
+  // @ts-ignore
+  fabricCanvas.value.wrapperEl.style.width = `${wrapSize.width}px`;
+  // @ts-ignore
+  fabricCanvas.value.wrapperEl.style.height = `${wrapSize.height}px`;
+  // @ts-ignore
+  fabricCanvas.value.lowerCanvasEl.style.width = `${wrapSize.width}px`;
+  // @ts-ignore
+  fabricCanvas.value.lowerCanvasEl.style.height = `${wrapSize.height}px`;
+  // @ts-ignore
+  fabricCanvas.value.upperCanvasEl.style.width = `${wrapSize.width}px`;
+  // @ts-ignore
+  fabricCanvas.value.upperCanvasEl.style.height = `${wrapSize.height}px`;
+}
+
+function initCanvas() {
+  const resolutionHeight =
+    currentResolutionRatio.value / window.devicePixelRatio;
+  const resolutionWidth =
+    (currentResolutionRatio.value / window.devicePixelRatio) * videoRatio.value;
+  const wrapWidth = containerRef.value!.getBoundingClientRect().width;
+  // const wrapWidth = 1920;
+  const ratio = wrapWidth / resolutionWidth;
+  scaleRatio.value = resolutionWidth / wrapWidth;
+  const wrapHeight = resolutionHeight * ratio;
+  // const wrapHeight = 1080;
+  // lower-canvas: 实际的canvas画面,也就是pushCanvasRef
+  // upper-canvas: 操作时候的canvas
+  const ins = markRaw(new fabric.Canvas(pushCanvasRef.value!));
+  ins.setWidth(resolutionWidth);
+  ins.setHeight(resolutionHeight);
+  ins.setBackgroundColor('black', () => {
+    console.log('setBackgroundColor回调');
+  });
+  wrapSize.width = wrapWidth;
+  wrapSize.height = wrapHeight;
+  fabricCanvas.value = ins;
+  changeCanvasStyle();
+}
+
+async function handleCache() {
+  const res: AppRootState['allTrack'] = [];
+  const queue: any[] = [];
+  resourceCacheStore.list.forEach((item) => {
+    // @ts-ignore
+    const obj: AppRootState['allTrack'][0] = {};
+    obj.audio = item.audio;
+    obj.video = item.video;
+    obj.id = item.id;
+    obj.type = item.type;
+    obj.hidden = item.hidden;
+    obj.mediaName = item.mediaName;
+    obj.muted = item.muted;
+    obj.rect = item.rect;
+    obj.scaleInfo = item.scaleInfo;
+
+    async function handleMediaVideo() {
+      const file = await readFile(item.id);
+      const url = URL.createObjectURL(file);
+      console.log(file, file.name, url);
+      const videoEl = createVideo({});
+      videoEl.src = url;
+      videoEl.muted = item.muted ? item.muted : false;
+      videoEl.style.width = `1px`;
+      videoEl.style.height = `1px`;
+      videoEl.style.position = 'fixed';
+      videoEl.style.bottom = '0';
+      videoEl.style.right = '0';
+      videoEl.style.opacity = '0';
+      videoEl.style.pointerEvents = 'none';
+      document.body.appendChild(videoEl);
+      await new Promise((resolve) => {
+        videoEl.onloadedmetadata = () => {
+          const stream = videoEl
+            // @ts-ignore
+            .captureStream();
+          const width = stream.getVideoTracks()[0].getSettings().width!;
+          const height = stream.getVideoTracks()[0].getSettings().height!;
+          // const ratio = handleScale({ width, height });
+          videoEl.width = width;
+          videoEl.height = height;
+
+          const canvasDom = markRaw(
+            new fabric.Image(videoEl, {
+              top: item.rect?.top || 0,
+              left: item.rect?.left || 0,
+              width,
+              height,
+            })
+          );
+          canvasDom.on('moving', () => {
+            appStore.allTrack.forEach((iten) => {
+              if (item.id === iten.id) {
+                iten.rect = { top: canvasDom.top!, left: canvasDom.left! };
+              }
+            });
+            resourceCacheStore.setList(appStore.allTrack);
+          });
+          canvasDom.scale(item.scaleInfo?.scaleX || 1);
+          fabricCanvas.value!.add(canvasDom);
+
+          renderFrame();
+          obj.videoEl = videoEl;
+          obj.canvasDom = canvasDom;
+          resolve({ videoEl, canvasDom });
+        };
+      });
+      const stream = videoEl
+        // @ts-ignore
+        .captureStream() as MediaStream;
+      obj.stream = stream;
+      obj.streamid = stream.id;
+      obj.track = stream.getVideoTracks()[0];
+      obj.trackid = stream.getVideoTracks()[0].id;
+      // if (stream.getAudioTracks()[0]) {
+      //   console.log('视频有音频', stream.getAudioTracks()[0]);
+      //   mediaVideoTrack.audio = 1;
+      //   const audioTrack: AppRootState['allTrack'][0] = {
+      //     id: mediaVideoTrack.id,
+      //     audio: 1,
+      //     video: 2,
+      //     mediaName: val.mediaName,
+      //     type: MediaTypeEnum.media,
+      //     track: stream.getAudioTracks()[0],
+      //     trackid: stream.getAudioTracks()[0].id,
+      //     stream,
+      //     streamid: stream.id,
+      //     hidden: true,
+      //     muted: false,
+      //   };
+      //   // @ts-ignore
+      //   const res = [...appStore.allTrack, audioTrack];
+      //   appStore.setAllTrack(res);
+      //   resourceCacheStore.setList(res);
+      //   handleMixedAudio();
+      //   // @ts-ignore
+
+      //   addTrack(audioTrack);
+      // }
+    }
+
+    async function handleImg() {
+      const file = await readFile(item.id);
+      const imgEl = await new Promise<HTMLImageElement>((resolve) => {
+        const reader = new FileReader();
+        reader.addEventListener(
+          'load',
+          function () {
+            const img = document.createElement('img');
+            img.src = reader.result as string;
+            img.onload = () => {
+              resolve(img);
+            };
+          },
+          false
+        );
+        if (file) {
+          reader.readAsDataURL(file);
+        }
+      });
+      if (fabricCanvas.value) {
+        const canvasDom = markRaw(
+          new fabric.Image(imgEl, {
+            top: item.rect?.top || 0,
+            left: item.rect?.left || 0,
+            width: imgEl.width,
+            height: imgEl.height,
+          })
+        );
+        canvasDom.on('moving', () => {
+          appStore.allTrack.forEach((item) => {
+            if (obj.id === item.id) {
+              item.rect = { top: canvasDom.top!, left: canvasDom.left! };
+            }
+          });
+          resourceCacheStore.setList(appStore.allTrack);
+        });
+        canvasDom.on('scaling', () => {
+          appStore.allTrack.forEach((item) => {
+            if (obj.id === item.id) {
+              item.scaleInfo = {
+                scaleX: canvasDom.scaleX || 1,
+                scalcY: canvasDom.scaleY || 1,
+              };
+            }
+          });
+          resourceCacheStore.setList(appStore.allTrack);
+        });
+        canvasDom.scale(item.scaleInfo?.scaleX || 1);
+        fabricCanvas.value.add(canvasDom);
+        obj.canvasDom = canvasDom;
+        renderFrame();
+      }
+    }
+    if (item.type === MediaTypeEnum.media && item.video === 1) {
+      queue.push(handleMediaVideo());
+    } else if (item.type === MediaTypeEnum.txt) {
+      obj.txtInfo = item.txtInfo;
+      if (fabricCanvas.value) {
+        const canvasDom = markRaw(
+          new fabric.Text(item.txtInfo?.txt || '', {
+            top: item.rect?.top || 0,
+            left: item.rect?.left || 0,
+            fill: item.txtInfo?.color,
+          })
+        );
+        canvasDom.on('moving', () => {
+          appStore.allTrack.forEach((item) => {
+            if (obj.id === item.id) {
+              item.rect = { top: canvasDom.top!, left: canvasDom.left! };
+            }
+          });
+          resourceCacheStore.setList(appStore.allTrack);
+        });
+        canvasDom.scale(item.scaleInfo?.scaleX || 1);
+        fabricCanvas.value.add(canvasDom);
+        obj.canvasDom = canvasDom;
+        renderFrame();
+      }
+    } else if (item.type === MediaTypeEnum.img) {
+      queue.push(handleImg());
+    }
+    res.push(obj);
+  });
+  await Promise.all(queue);
+  canvasVideoStream.value = pushCanvasRef.value!.captureStream();
+  appStore.setAllTrack(res);
+}
+
+onMounted(() => {
+  setTimeout(() => {
+    scrollTo(0, 0);
+  }, 100);
+  // initNullAudio();
+  initUserMedia();
+  initCanvas();
+  handleCache();
+  document.addEventListener('visibilitychange', onPageVisibility);
+});
+
+onUnmounted(() => {
+  document.removeEventListener('visibilitychange', onPageVisibility);
+});
+
+function selectMediaOk(val: MediaTypeEnum) {
+  showMediaModalCpt.value = true;
+  showSelectMediaModalCpt.value = false;
+  currentMediaType.value = val;
+}
+
+async function addMediaOk(val: {
+  type: MediaTypeEnum;
+  deviceId: string;
+  mediaName: string;
+  txtInfo?: { txt: string; color: string };
+  imgInfo?: UploadFileInfo[];
+  mediaInfo?: UploadFileInfo[];
+}) {
+  if (!audioCtx.value) {
+    audioCtx.value = new AudioContext();
+  }
+  showMediaModalCpt.value = false;
+  if (val.type === MediaTypeEnum.screen) {
+    const event = await navigator.mediaDevices.getDisplayMedia({
+      video: {
+        deviceId: val.deviceId,
+        // displaySurface: 'monitor', // browser默认标签页;window默认窗口;monitor默认整个屏幕
+      },
+      audio: true,
+    });
+
+    const videoTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.screen,
+      track: event.getVideoTracks()[0],
+      trackid: event.getVideoTracks()[0].id,
+      stream: event,
+      streamid: event.id,
+      hidden: false,
+      muted: false,
+    };
+
+    const { canvasDom, videoEl } = await autoCreateVideo({
+      stream: event,
+      id: videoTrack.id,
+    });
+    videoTrack.videoEl = videoEl;
+    // @ts-ignore
+    videoTrack.canvasDom = canvasDom;
+
+    const audio = event.getAudioTracks();
+    if (audio.length) {
+      videoTrack.audio = 1;
+      const audioTrack: AppRootState['allTrack'][0] = {
+        id: videoTrack.id,
+        audio: 1,
+        video: 2,
+        mediaName: val.mediaName,
+        type: MediaTypeEnum.screen,
+        track: event.getAudioTracks()[0],
+        trackid: event.getAudioTracks()[0].id,
+        stream: event,
+        streamid: event.id,
+        hidden: true,
+        muted: false,
+      };
+      const res = [...appStore.allTrack, videoTrack, audioTrack];
+      appStore.setAllTrack(res);
+      resourceCacheStore.setList(res);
+      handleMixedAudio();
+      // @ts-ignore
+      addTrack(videoTrack);
+      // @ts-ignore
+      addTrack(audioTrack);
+    } else {
+      const res = [...appStore.allTrack, videoTrack];
+      appStore.setAllTrack(res);
+      resourceCacheStore.setList(res);
+      // @ts-ignore
+      addTrack(videoTrack);
+    }
+
+    console.log('获取窗口成功');
+  } else if (val.type === MediaTypeEnum.camera) {
+    const event = await navigator.mediaDevices.getUserMedia({
+      video: {
+        deviceId: val.deviceId,
+      },
+      audio: false,
+    });
+    const videoTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.camera,
+      track: event.getVideoTracks()[0],
+      trackid: event.getVideoTracks()[0].id,
+      stream: event,
+      streamid: event.id,
+      hidden: false,
+      muted: false,
+    };
+    const { canvasDom, videoEl } = await autoCreateVideo({
+      stream: event,
+      id: videoTrack.id,
+    });
+    videoTrack.videoEl = videoEl;
+    // @ts-ignore
+    videoTrack.canvasDom = canvasDom;
+
+    const res = [...appStore.allTrack, videoTrack];
+    appStore.setAllTrack(res);
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(videoTrack);
+    console.log('获取摄像头成功');
+  } else if (val.type === MediaTypeEnum.microphone) {
+    const event = await navigator.mediaDevices.getUserMedia({
+      video: false,
+      audio: { deviceId: val.deviceId },
+    });
+    const audioTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 1,
+      video: 2,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.microphone,
+      track: event.getAudioTracks()[0],
+      trackid: event.getAudioTracks()[0].id,
+      stream: event,
+      streamid: event.id,
+      hidden: false,
+      muted: false,
+    };
+    const res = [...appStore.allTrack, audioTrack];
+    appStore.setAllTrack(res);
+    resourceCacheStore.setList(res);
+    handleMixedAudio();
+    // @ts-ignore
+    addTrack(audioTrack);
+
+    console.log('获取麦克风成功');
+  } else if (val.type === MediaTypeEnum.txt) {
+    const txtTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.txt,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+    };
+    if (fabricCanvas.value) {
+      const canvasDom = markRaw(
+        new fabric.Text(val.txtInfo?.txt || '', {
+          top: 0,
+          left: 0,
+          fill: val.txtInfo?.color,
+        })
+      );
+      canvasDom.on('moving', () => {
+        appStore.allTrack.forEach((item) => {
+          if (txtTrack.id === item.id) {
+            item.rect = { top: canvasDom.top!, left: canvasDom.left! };
+          }
+        });
+        resourceCacheStore.setList(appStore.allTrack);
+      });
+      txtTrack.txtInfo = val.txtInfo;
+      // @ts-ignore
+      txtTrack.canvasDom = canvasDom;
+      fabricCanvas.value.add(canvasDom);
+      renderFrame();
+    }
+
+    const res = [...appStore.allTrack, txtTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(txtTrack);
+
+    console.log('获取文字成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.img) {
+    const imgTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.img,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+    };
+
+    if (fabricCanvas.value) {
+      const imgEl = await new Promise<HTMLImageElement>((resolve) => {
+        if (!val.imgInfo) return;
+        const file = val.imgInfo[0].file!;
+        saveFile({ file, fileName: imgTrack.id });
+        const reader = new FileReader();
+        reader.addEventListener(
+          'load',
+          function () {
+            const img = document.createElement('img');
+            img.src = reader.result as string;
+            img.onload = () => {
+              resolve(img);
+            };
+          },
+          false
+        );
+        if (file) {
+          reader.readAsDataURL(file);
+        }
+      });
+
+      const canvasDom = markRaw(
+        new fabric.Image(imgEl, {
+          top: 0,
+          left: 0,
+          width: imgEl.width,
+          height: imgEl.height,
+        })
+      );
+      canvasDom.on('moving', () => {
+        appStore.allTrack.forEach((item) => {
+          if (imgTrack.id === item.id) {
+            item.rect = { top: canvasDom.top!, left: canvasDom.left! };
+          }
+        });
+        resourceCacheStore.setList(appStore.allTrack);
+      });
+      const ratio = handleScale({ width: imgEl.width, height: imgEl.height });
+      // @ts-ignore
+      imgTrack.canvasDom = canvasDom;
+      imgTrack.scaleInfo = { scaleX: ratio, scalcY: ratio };
+      canvasDom.scale(ratio);
+      fabricCanvas.value.add(canvasDom);
+      renderFrame();
+    }
+
+    const res = [...appStore.allTrack, imgTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+    addTrack(imgTrack);
+
+    console.log('获取图片成功', fabricCanvas.value);
+  } else if (val.type === MediaTypeEnum.media) {
+    const mediaVideoTrack: AppRootState['allTrack'][0] = {
+      id: getRandomEnglishString(8),
+      audio: 2,
+      video: 1,
+      mediaName: val.mediaName,
+      type: MediaTypeEnum.media,
+      track: undefined,
+      trackid: undefined,
+      stream: undefined,
+      streamid: undefined,
+      hidden: false,
+      muted: false,
+    };
+    if (fabricCanvas.value) {
+      if (!val.mediaInfo) return;
+      const file = val.mediaInfo[0].file!;
+      saveFile({ file, fileName: mediaVideoTrack.id });
+      const url = URL.createObjectURL(file);
+      console.log(file, file.name, url);
+      const videoEl = createVideo({});
+      videoEl.src = url;
+      videoEl.muted = false;
+      videoEl.style.width = `1px`;
+      videoEl.style.height = `1px`;
+      videoEl.style.position = 'fixed';
+      videoEl.style.bottom = '0';
+      videoEl.style.right = '0';
+      videoEl.style.opacity = '0';
+      videoEl.style.pointerEvents = 'none';
+      document.body.appendChild(videoEl);
+      const videoRes = await new Promise<HTMLVideoElement>((resolve) => {
+        videoEl.onloadedmetadata = () => {
+          resolve(videoEl);
+        };
+      });
+      // @ts-ignore
+      const stream = videoRes.captureStream();
+      const { canvasDom } = await autoCreateVideo({
+        stream,
+        id: mediaVideoTrack.id,
+      });
+      mediaVideoTrack.videoEl = videoEl;
+      // @ts-ignore
+      mediaVideoTrack.canvasDom = canvasDom;
+      if (stream.getAudioTracks()[0]) {
+        console.log('视频有音频', stream.getAudioTracks()[0]);
+        mediaVideoTrack.audio = 1;
+        const audioTrack: AppRootState['allTrack'][0] = {
+          id: mediaVideoTrack.id,
+          audio: 1,
+          video: 2,
+          mediaName: val.mediaName,
+          type: MediaTypeEnum.media,
+          track: stream.getAudioTracks()[0],
+          trackid: stream.getAudioTracks()[0].id,
+          stream,
+          streamid: stream.id,
+          hidden: true,
+          muted: false,
+        };
+        // @ts-ignore
+        const res = [...appStore.allTrack, audioTrack];
+        appStore.setAllTrack(res);
+        resourceCacheStore.setList(res);
+        handleMixedAudio();
+        // @ts-ignore
+
+        addTrack(audioTrack);
+      }
+    }
+    const res = [...appStore.allTrack, mediaVideoTrack];
+    // @ts-ignore
+    appStore.setAllTrack(res);
+    // @ts-ignore
+    resourceCacheStore.setList(res);
+    // @ts-ignore
+
+    addTrack(mediaVideoTrack);
+
+    console.log('获取视频成功', fabricCanvas.value);
+  }
+
+  canvasVideoStream.value = pushCanvasRef.value!.captureStream();
+}
+
+function handleChangeMuted(item: AppRootState['allTrack'][0]) {
+  console.log('handleChangeMuted', item);
+  if (item.videoEl) {
+    const res = !item.videoEl.muted;
+    item.videoEl.muted = res;
+    item.muted = res;
+    resourceCacheStore.setList(appStore.allTrack);
+  }
+}
+
+function handleEdit(item: AppRootState['allTrack'][0]) {
+  console.log('handleEdit', item);
+}
+
+function handleDel(item: AppRootState['allTrack'][0]) {
+  console.log('handleDel', item);
+  if (item.canvasDom !== undefined) {
+    // @ts-ignore
+    fabricCanvas.value?.remove(item.canvasDom);
+    item.videoEl?.remove();
+  }
+  const res = appStore.allTrack.filter((iten) => iten.id !== item.id);
+  appStore.setAllTrack(res);
+  resourceCacheStore.setList(res);
+  delTrack(item);
+}
+
+function saveFile(data: { file: File; fileName: string }) {
+  const { file, fileName } = data;
+  const requestFileSystem =
+    // @ts-ignore
+    window.requestFileSystem || window.webkitRequestFileSystem;
+  function onError(err) {
+    console.error('saveFile错误', data.fileName);
+    console.log(err);
+  }
+  function onFs(fs) {
+    // 创建文件
+    fs.root.getFile(
+      fileName,
+      { create: true },
+      function (fileEntry) {
+        // 创建文件写入流
+        fileEntry.createWriter(function (fileWriter) {
+          fileWriter.onwriteend = () => {
+            // 完成后关闭文件
+            fileWriter.abort();
+          };
+          // 写入文件内容
+          fileWriter.write(file);
+        });
+      },
+      () => {
+        console.log('写入文件失败');
+      }
+    );
+  }
+  // Opening a file system with temporary storage
+  requestFileSystem(
+    // @ts-ignore
+    window.PERSISTENT,
+    0,
+    onFs,
+    onError
+  );
+}
+
+function readFile(fileName: string) {
+  return new Promise<File>((resolve) => {
+    const requestFileSystem =
+      // @ts-ignore
+      window.requestFileSystem || window.webkitRequestFileSystem;
+    function onError(err) {
+      console.error('readFile错误', fileName);
+      console.log(err);
+    }
+    function onFs(fs) {
+      fs.root.getFile(
+        fileName,
+        {},
+        function (fileEntry) {
+          fileEntry.file(function (file) {
+            resolve(file);
+            // const url = URL.createObjectURL(file);
+            // const videoEl = createVideo({});
+            // videoEl.src = url;
+            // document.body.appendChild(videoEl);
+          }, onError);
+        },
+        onError
+      );
+    }
+    // Opening a file system with temporary storage
+    requestFileSystem(
+      // @ts-ignore
+      window.PERSISTENT,
+      0,
+      onFs,
+      onError
+    );
+  });
+}
+
+function handleStartMedia(item: { type: MediaTypeEnum; txt: string }) {
+  currentMediaType.value = item.type;
+  showMediaModalCpt.value = true;
+}
+</script>
+
+<style lang="scss" scoped>
+.push-wrap {
+  display: flex;
+  justify-content: space-between;
+  margin: 15px auto 0;
+  width: $w-1250;
+  .left {
+    position: relative;
+    display: inline-block;
+    overflow: hidden;
+    box-sizing: border-box;
+    width: $w-960;
+    height: 100%;
+    border-radius: 6px;
+    background-color: white;
+    color: #9499a0;
+    vertical-align: top;
+
+    .container {
+      position: relative;
+      overflow: hidden;
+      height: 100%;
+      background-color: rgba($color: #000000, $alpha: 0.5);
+      line-height: 0;
+
+      :deep(canvas) {
+        // width: 100%;
+      }
+
+      .add-wrap {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: space-around;
+        padding: 0 20px;
+        height: 50px;
+        border-radius: 6px;
+        background-color: white;
+        transform: translate(-50%, -50%);
+      }
+    }
+    .room-control {
+      display: flex;
+      justify-content: space-between;
+      padding: 20px;
+      background-color: papayawhip;
+
+      .info {
+        display: flex;
+        align-items: center;
+
+        .avatar {
+          margin-right: 20px;
+          width: 55px;
+          height: 55px;
+          border-radius: 50%;
+          background-position: center center;
+          background-size: cover;
+          background-repeat: no-repeat;
+        }
+        .detail {
+          display: flex;
+          flex-direction: column;
+          flex-shrink: 0;
+          width: 200px;
+          text-align: initial;
+          .top {
+            margin-bottom: 10px;
+            color: #18191c;
+          }
+          .bottom {
+            font-size: 14px;
+          }
+        }
+      }
+      .rtc {
+        display: flex;
+        align-items: center;
+        flex: 1;
+        font-size: 14px;
+        .item {
+          display: flex;
+          align-items: center;
+          flex: 1;
+          .txt {
+            flex-shrink: 0;
+            width: 80px;
+          }
+          .down {
+            width: 90px;
+
+            user-select: none;
+          }
+        }
+      }
+      .other {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        font-size: 12px;
+        .top {
+        }
+        .bottom {
+          margin-top: 10px;
+        }
+      }
+    }
+  }
+  .right {
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    box-sizing: border-box;
+    margin-left: 10px;
+    width: $w-250;
+    border-radius: 6px;
+    background-color: white;
+    color: #9499a0;
+
+    .resource-card {
+      position: relative;
+      box-sizing: border-box;
+      margin-bottom: 10px;
+      padding: 10px;
+      width: 100%;
+      height: 290px;
+      border-radius: 6px;
+      background-color: papayawhip;
+      .title {
+        text-align: initial;
+      }
+      .item {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        margin: 5px 0;
+        font-size: 14px;
+        // &:hover {
+        //   .control {
+        //     display: flex;
+        //     align-items: center;
+        //   }
+        // }
+        .control {
+          display: flex;
+          align-items: center;
+          .control-item {
+            cursor: pointer;
+            &:not(:last-child) {
+              margin-right: 6px;
+            }
+          }
+        }
+      }
+      .bottom {
+        position: absolute;
+        bottom: 0;
+        left: 0;
+        padding: 10px;
+      }
+    }
+    .danmu-card {
+      position: relative;
+      flex: 1;
+      box-sizing: border-box;
+      padding: 10px;
+      width: 100%;
+      border-radius: 6px;
+      background-color: papayawhip;
+      text-align: initial;
+      .title {
+        margin-bottom: 10px;
+      }
+      .list {
+        overflow: scroll;
+        height: 360px;
+
+        @extend %hideScrollbar;
+
+        .item {
+          margin-bottom: 10px;
+          font-size: 12px;
+          .name {
+            color: #9499a0;
+          }
+          .msg {
+            color: #61666d;
+          }
+        }
+      }
+
+      .send-msg {
+        position: absolute;
+        bottom: 10px;
+        left: 50%;
+        display: flex;
+        align-items: center;
+        box-sizing: border-box;
+        width: calc(100% - 20px);
+        transform: translateX(-50%);
+        .ipt {
+          display: block;
+          box-sizing: border-box;
+          margin: 0 auto;
+          margin-right: 10px;
+          padding: 10px;
+          width: 80%;
+          height: 30px;
+          outline: none;
+          border: 1px solid hsla(0, 0%, 60%, 0.2);
+          border-radius: 4px;
+          background-color: #f1f2f3;
+          font-size: 14px;
+        }
+      }
+    }
+  }
+}
+
+// 屏幕宽度大于1500的时候
+@media screen and (min-width: $w-1500) {
+  .push-wrap {
+    width: $w-1475;
+    .left {
+      width: $w-1150;
+    }
+    .right {
+      width: $w-300;
+    }
+  }
+}
+</style>

+ 1 - 1
src/views/pushByCanvas/mediaModal/index.vue

@@ -88,7 +88,7 @@
 
 <script lang="ts" setup>
 import { InputInst, UploadFileInfo } from 'naive-ui';
-import { defineEmits, defineProps, onMounted, ref, withDefaults } from 'vue';
+import { onMounted, ref } from 'vue';
 
 import { MediaTypeEnum } from '@/interface';
 import { useAppStore } from '@/store/app';

+ 1 - 1
src/views/pushByCanvas/openMicophoneTip/index.vue

@@ -14,7 +14,7 @@
 </template>
 
 <script lang="ts" setup>
-import { defineEmits, onMounted } from 'vue';
+import { onMounted } from 'vue';
 
 const emits = defineEmits(['close']);
 

+ 1 - 1
src/views/pushByCanvas/selectMediaModal/index.vue

@@ -23,7 +23,7 @@
 </template>
 
 <script lang="ts" setup>
-import { defineEmits, defineProps, onMounted, withDefaults } from 'vue';
+import { onMounted } from 'vue';
 
 import { MediaTypeEnum } from '@/interface';
 

+ 1 - 0
tsconfig.json

@@ -13,6 +13,7 @@
     "resolveJsonModule": true, //解析json模块
     "allowJs": true,
     "checkJs": true,
+    // "noErrorTruncation": true, // https://github.com/vuejs/language-tools/issues/2533#issuecomment-1543496140
     "baseUrl": "./",
     /**
      * 当 TypeScript 编译文件时,它在输出目录中保持与输入目录中相同的目录结构。

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor