소스 검색

带看功能

tremble 3 년 전
부모
커밋
42d6671f8a
68개의 변경된 파일5898개의 추가작업 그리고 55개의 파일을 삭제
  1. 1 2
      .env.prod
  2. 236 26
      package-lock.json
  3. 3 2
      package.json
  4. 1 0
      public/index.html
  5. 2060 0
      public/viewer/static/lib/socket.io.min.js
  6. 2 2
      src/apis/index.js
  7. 16 4
      src/app.vue
  8. BIN
      src/assets/images/rtcLive/Input_disabled@2x.png
  9. BIN
      src/assets/images/rtcLive/Input_norma@2x.png
  10. BIN
      src/assets/images/rtcLive/arrows@2x.png
  11. BIN
      src/assets/images/rtcLive/avatar_small@2x.png
  12. BIN
      src/assets/images/rtcLive/brushes@2x.png
  13. BIN
      src/assets/images/rtcLive/brushes_selected@2_1.png
  14. BIN
      src/assets/images/rtcLive/brushes_selected@2x.png
  15. BIN
      src/assets/images/rtcLive/chat_off@2x.png
  16. BIN
      src/assets/images/rtcLive/chat_on@2x.png
  17. BIN
      src/assets/images/rtcLive/chrome.png
  18. BIN
      src/assets/images/rtcLive/cross@2x.png
  19. BIN
      src/assets/images/rtcLive/edge.png
  20. BIN
      src/assets/images/rtcLive/exit@2x.png
  21. BIN
      src/assets/images/rtcLive/firefox.png
  22. BIN
      src/assets/images/rtcLive/guided@2x.png
  23. BIN
      src/assets/images/rtcLive/hot_spot@2x.png
  24. BIN
      src/assets/images/rtcLive/hot_spot_selected@2x.png
  25. BIN
      src/assets/images/rtcLive/invitation@2x.png
  26. BIN
      src/assets/images/rtcLive/loading@2x.png
  27. BIN
      src/assets/images/rtcLive/members@2x.png
  28. BIN
      src/assets/images/rtcLive/mic_all_off@2x.png
  29. BIN
      src/assets/images/rtcLive/mic_all_on@2x.png
  30. BIN
      src/assets/images/rtcLive/mic_off@2x.png
  31. BIN
      src/assets/images/rtcLive/mic_off_50@2x.png
  32. BIN
      src/assets/images/rtcLive/mic_off_black@2x.png
  33. BIN
      src/assets/images/rtcLive/mic_on@2x.png
  34. BIN
      src/assets/images/rtcLive/pop-up_screen_off@2x.png
  35. BIN
      src/assets/images/rtcLive/pop-up_screen_on@2x.png
  36. BIN
      src/assets/images/rtcLive/remove@2x.png
  37. BIN
      src/assets/images/rtcLive/revocation@2x.png
  38. BIN
      src/assets/images/rtcLive/safari.png
  39. BIN
      src/assets/images/rtcLive/scene_mic_off@2x.png
  40. BIN
      src/assets/images/rtcLive/scene_mic_on@2x.png
  41. BIN
      src/assets/images/rtcLive/send@2x.png
  42. BIN
      src/assets/images/rtcLive/send_norma@2x.png
  43. BIN
      src/assets/images/rtcLive/send_selected@2x.png
  44. BIN
      src/assets/images/rtcLive/showing@2x.png
  45. BIN
      src/assets/images/rtcLive/speed.png
  46. BIN
      src/assets/images/rtcLive/video_off@2x.png
  47. BIN
      src/assets/images/rtcLive/video_off_50@2x.png
  48. BIN
      src/assets/images/rtcLive/video_on@2x.png
  49. BIN
      src/assets/images/rtcLive/vr@2x.png
  50. 10 2
      src/components/Controls/Control.Mobile.vue
  51. 5 1
      src/components/Controls/Panel/Main.vue
  52. 334 0
      src/components/RTC/PageHome.vue
  53. 928 0
      src/components/RTC/PageRtcLive.vue
  54. 640 0
      src/components/RTC/PageShopping.vue
  55. 80 0
      src/components/RTC/chat/chat.vue
  56. 94 0
      src/components/RTC/dialog/checkBrowser.vue
  57. 279 0
      src/components/RTC/dialog/createdRoom.vue
  58. 138 0
      src/components/RTC/dialog/index.vue
  59. 169 0
      src/components/RTC/dialog/share.vue
  60. 73 0
      src/components/RTC/index.vue
  61. 481 0
      src/components/RTC/paint/Draw.vue
  62. 308 0
      src/components/RTC/paint/index.vue
  63. 9 0
      src/components/RTC/socket/index.js
  64. 5 0
      src/store/index.js
  65. 3 1
      src/store/modules/scene.js
  66. 19 15
      src/utils/browser.js
  67. 1 0
      src/utils/rtc_socket.js
  68. 3 0
      src/views/Panel.vue

+ 1 - 2
.env.prod

@@ -1,6 +1,5 @@
-
 # 场景资源地址
-VUE_APP_RESOURCE_URL=https://eurs3.4dkankan.com/
+VUE_APP_RESOURCE_URL=https://glp-vr.cdfmembers.com/
 # 静态资源地址
 VUE_APP_CDN_URL=https://4dkk.4dage.com/v4/www/
 # sdk文件地址

+ 236 - 26
package-lock.json

@@ -11,6 +11,7 @@
         "axios": "^0.21.1",
         "clipboard": "^2.0.8",
         "core-js": "^3.8.3",
+        "socket.io": "^4.5.1",
         "vue": "^3.2.36",
         "vuex": "^4.0.2"
       },
@@ -1947,6 +1948,11 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/component-emitter": {
+      "version": "1.2.11",
+      "resolved": "https://registry.npmmirror.com/@types/component-emitter/-/component-emitter-1.2.11.tgz",
+      "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ=="
+    },
     "node_modules/@types/connect": {
       "version": "3.4.35",
       "resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.35.tgz",
@@ -1966,6 +1972,16 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/cookie": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmmirror.com/@types/cookie/-/cookie-0.4.1.tgz",
+      "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
+    },
+    "node_modules/@types/cors": {
+      "version": "2.8.12",
+      "resolved": "https://registry.npmmirror.com/@types/cors/-/cors-2.8.12.tgz",
+      "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw=="
+    },
     "node_modules/@types/eslint": {
       "version": "7.29.0",
       "resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-7.29.0.tgz",
@@ -2051,8 +2067,7 @@
     "node_modules/@types/node": {
       "version": "17.0.35",
       "resolved": "https://registry.npmmirror.com/@types/node/-/node-17.0.35.tgz",
-      "integrity": "sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg==",
-      "dev": true
+      "integrity": "sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg=="
     },
     "node_modules/@types/normalize-package-data": {
       "version": "2.4.1",
@@ -2962,7 +2977,6 @@
       "version": "1.3.8",
       "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
       "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
-      "dev": true,
       "dependencies": {
         "mime-types": "~2.1.34",
         "negotiator": "0.6.3"
@@ -3290,6 +3304,14 @@
       "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
       "dev": true
     },
+    "node_modules/base64id": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/base64id/-/base64id-2.0.0.tgz",
+      "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
+      "engines": {
+        "node": "^4.5.0 || >= 5.9"
+      }
+    },
     "node_modules/batch": {
       "version": "0.6.1",
       "resolved": "https://registry.npmmirror.com/batch/-/batch-0.6.1.tgz",
@@ -3811,6 +3833,11 @@
       "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
       "dev": true
     },
+    "node_modules/component-emitter": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/component-emitter/-/component-emitter-1.3.0.tgz",
+      "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
+    },
     "node_modules/compressible": {
       "version": "2.0.18",
       "resolved": "https://registry.npmmirror.com/compressible/-/compressible-2.0.18.tgz",
@@ -3999,6 +4026,18 @@
       "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
       "dev": true
     },
+    "node_modules/cors": {
+      "version": "2.8.5",
+      "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.5.tgz",
+      "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+      "dependencies": {
+        "object-assign": "^4",
+        "vary": "^1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
     "node_modules/cosmiconfig": {
       "version": "7.0.1",
       "resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz",
@@ -4308,7 +4347,6 @@
       "version": "4.3.4",
       "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz",
       "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
-      "dev": true,
       "dependencies": {
         "ms": "2.1.2"
       },
@@ -4676,6 +4714,62 @@
         "once": "^1.4.0"
       }
     },
+    "node_modules/engine.io": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmmirror.com/engine.io/-/engine.io-6.2.0.tgz",
+      "integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==",
+      "dependencies": {
+        "@types/cookie": "^0.4.1",
+        "@types/cors": "^2.8.12",
+        "@types/node": ">=10.0.0",
+        "accepts": "~1.3.4",
+        "base64id": "2.0.0",
+        "cookie": "~0.4.1",
+        "cors": "~2.8.5",
+        "debug": "~4.3.1",
+        "engine.io-parser": "~5.0.3",
+        "ws": "~8.2.3"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/engine.io-parser": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
+      "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/engine.io/node_modules/cookie": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.4.2.tgz",
+      "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/engine.io/node_modules/ws": {
+      "version": "8.2.3",
+      "resolved": "https://registry.npmmirror.com/ws/-/ws-8.2.3.tgz",
+      "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": "^5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/enhanced-resolve": {
       "version": "5.9.3",
       "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz",
@@ -6383,7 +6477,6 @@
       "version": "1.52.0",
       "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
       "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
-      "dev": true,
       "engines": {
         "node": ">= 0.6"
       }
@@ -6392,7 +6485,6 @@
       "version": "2.1.35",
       "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
       "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
-      "dev": true,
       "dependencies": {
         "mime-db": "1.52.0"
       },
@@ -6535,8 +6627,7 @@
     "node_modules/ms": {
       "version": "2.1.2",
       "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz",
-      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-      "dev": true
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
     "node_modules/multicast-dns": {
       "version": "7.2.5",
@@ -6577,7 +6668,6 @@
       "version": "0.6.3",
       "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz",
       "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
-      "dev": true,
       "engines": {
         "node": ">= 0.6"
       }
@@ -6712,7 +6802,6 @@
       "version": "4.1.1",
       "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
       "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
-      "dev": true,
       "engines": {
         "node": ">=0.10.0"
       }
@@ -8416,6 +8505,40 @@
         "node": ">=8"
       }
     },
+    "node_modules/socket.io": {
+      "version": "4.5.1",
+      "resolved": "https://registry.npmmirror.com/socket.io/-/socket.io-4.5.1.tgz",
+      "integrity": "sha512-0y9pnIso5a9i+lJmsCdtmTTgJFFSvNQKDnPQRz28mGNnxbmqYg2QPtJTLFxhymFZhAIn50eHAKzJeiNaKr+yUQ==",
+      "dependencies": {
+        "accepts": "~1.3.4",
+        "base64id": "~2.0.0",
+        "debug": "~4.3.2",
+        "engine.io": "~6.2.0",
+        "socket.io-adapter": "~2.4.0",
+        "socket.io-parser": "~4.0.4"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/socket.io-adapter": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmmirror.com/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz",
+      "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg=="
+    },
+    "node_modules/socket.io-parser": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-4.0.5.tgz",
+      "integrity": "sha512-sNjbT9dX63nqUFIOv95tTVm6elyIU4RvB1m8dOeZt+IgWwcWklFDOdmGcfo3zSiRsnR/3pJkjY5lfoGqEe4Eig==",
+      "dependencies": {
+        "@types/component-emitter": "^1.2.10",
+        "component-emitter": "~1.3.0",
+        "debug": "~4.3.1"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
     "node_modules/sockjs": {
       "version": "0.3.24",
       "resolved": "https://registry.npmmirror.com/sockjs/-/sockjs-0.3.24.tgz",
@@ -9063,7 +9186,6 @@
       "version": "1.1.2",
       "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
       "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
-      "dev": true,
       "engines": {
         "node": ">= 0.8"
       }
@@ -11180,6 +11302,11 @@
         "@types/node": "*"
       }
     },
+    "@types/component-emitter": {
+      "version": "1.2.11",
+      "resolved": "https://registry.npmmirror.com/@types/component-emitter/-/component-emitter-1.2.11.tgz",
+      "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ=="
+    },
     "@types/connect": {
       "version": "3.4.35",
       "resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.35.tgz",
@@ -11199,6 +11326,16 @@
         "@types/node": "*"
       }
     },
+    "@types/cookie": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmmirror.com/@types/cookie/-/cookie-0.4.1.tgz",
+      "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
+    },
+    "@types/cors": {
+      "version": "2.8.12",
+      "resolved": "https://registry.npmmirror.com/@types/cors/-/cors-2.8.12.tgz",
+      "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw=="
+    },
     "@types/eslint": {
       "version": "7.29.0",
       "resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-7.29.0.tgz",
@@ -11284,8 +11421,7 @@
     "@types/node": {
       "version": "17.0.35",
       "resolved": "https://registry.npmmirror.com/@types/node/-/node-17.0.35.tgz",
-      "integrity": "sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg==",
-      "dev": true
+      "integrity": "sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg=="
     },
     "@types/normalize-package-data": {
       "version": "2.4.1",
@@ -12068,7 +12204,6 @@
       "version": "1.3.8",
       "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
       "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
-      "dev": true,
       "requires": {
         "mime-types": "~2.1.34",
         "negotiator": "0.6.3"
@@ -12321,6 +12456,11 @@
       "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
       "dev": true
     },
+    "base64id": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/base64id/-/base64id-2.0.0.tgz",
+      "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
+    },
     "batch": {
       "version": "0.6.1",
       "resolved": "https://registry.npmmirror.com/batch/-/batch-0.6.1.tgz",
@@ -12754,6 +12894,11 @@
       "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
       "dev": true
     },
+    "component-emitter": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/component-emitter/-/component-emitter-1.3.0.tgz",
+      "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
+    },
     "compressible": {
       "version": "2.0.18",
       "resolved": "https://registry.npmmirror.com/compressible/-/compressible-2.0.18.tgz",
@@ -12916,6 +13061,15 @@
       "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
       "dev": true
     },
+    "cors": {
+      "version": "2.8.5",
+      "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.5.tgz",
+      "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+      "requires": {
+        "object-assign": "^4",
+        "vary": "^1"
+      }
+    },
     "cosmiconfig": {
       "version": "7.0.1",
       "resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz",
@@ -13147,7 +13301,6 @@
       "version": "4.3.4",
       "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz",
       "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
-      "dev": true,
       "requires": {
         "ms": "2.1.2"
       }
@@ -13439,6 +13592,41 @@
         "once": "^1.4.0"
       }
     },
+    "engine.io": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmmirror.com/engine.io/-/engine.io-6.2.0.tgz",
+      "integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==",
+      "requires": {
+        "@types/cookie": "^0.4.1",
+        "@types/cors": "^2.8.12",
+        "@types/node": ">=10.0.0",
+        "accepts": "~1.3.4",
+        "base64id": "2.0.0",
+        "cookie": "~0.4.1",
+        "cors": "~2.8.5",
+        "debug": "~4.3.1",
+        "engine.io-parser": "~5.0.3",
+        "ws": "~8.2.3"
+      },
+      "dependencies": {
+        "cookie": {
+          "version": "0.4.2",
+          "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.4.2.tgz",
+          "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="
+        },
+        "ws": {
+          "version": "8.2.3",
+          "resolved": "https://registry.npmmirror.com/ws/-/ws-8.2.3.tgz",
+          "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
+          "requires": {}
+        }
+      }
+    },
+    "engine.io-parser": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
+      "integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg=="
+    },
     "enhanced-resolve": {
       "version": "5.9.3",
       "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz",
@@ -14818,14 +15006,12 @@
     "mime-db": {
       "version": "1.52.0",
       "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
-      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
-      "dev": true
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
     },
     "mime-types": {
       "version": "2.1.35",
       "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
       "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
-      "dev": true,
       "requires": {
         "mime-db": "1.52.0"
       }
@@ -14940,8 +15126,7 @@
     "ms": {
       "version": "2.1.2",
       "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz",
-      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-      "dev": true
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
     },
     "multicast-dns": {
       "version": "7.2.5",
@@ -14972,8 +15157,7 @@
     "negotiator": {
       "version": "0.6.3",
       "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz",
-      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
-      "dev": true
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
     },
     "neo-async": {
       "version": "2.6.2",
@@ -15077,8 +15261,7 @@
     "object-assign": {
       "version": "4.1.1",
       "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
-      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
-      "dev": true
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
     },
     "object-inspect": {
       "version": "1.12.1",
@@ -16365,6 +16548,34 @@
       "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
       "dev": true
     },
+    "socket.io": {
+      "version": "4.5.1",
+      "resolved": "https://registry.npmmirror.com/socket.io/-/socket.io-4.5.1.tgz",
+      "integrity": "sha512-0y9pnIso5a9i+lJmsCdtmTTgJFFSvNQKDnPQRz28mGNnxbmqYg2QPtJTLFxhymFZhAIn50eHAKzJeiNaKr+yUQ==",
+      "requires": {
+        "accepts": "~1.3.4",
+        "base64id": "~2.0.0",
+        "debug": "~4.3.2",
+        "engine.io": "~6.2.0",
+        "socket.io-adapter": "~2.4.0",
+        "socket.io-parser": "~4.0.4"
+      }
+    },
+    "socket.io-adapter": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmmirror.com/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz",
+      "integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg=="
+    },
+    "socket.io-parser": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-4.0.5.tgz",
+      "integrity": "sha512-sNjbT9dX63nqUFIOv95tTVm6elyIU4RvB1m8dOeZt+IgWwcWklFDOdmGcfo3zSiRsnR/3pJkjY5lfoGqEe4Eig==",
+      "requires": {
+        "@types/component-emitter": "^1.2.10",
+        "component-emitter": "~1.3.0",
+        "debug": "~4.3.1"
+      }
+    },
     "sockjs": {
       "version": "0.3.24",
       "resolved": "https://registry.npmmirror.com/sockjs/-/sockjs-0.3.24.tgz",
@@ -16884,8 +17095,7 @@
     "vary": {
       "version": "1.1.2",
       "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
-      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
-      "dev": true
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
     },
     "vue": {
       "version": "3.2.36",

+ 3 - 2
package.json

@@ -9,10 +9,11 @@
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
-    "core-js": "^3.8.3",
-    "vue": "^3.2.36",
     "axios": "^0.21.1",
     "clipboard": "^2.0.8",
+    "core-js": "^3.8.3",
+    "socket.io": "^4.5.1",
+    "vue": "^3.2.36",
     "vuex": "^4.0.2"
   },
   "devDependencies": {

+ 1 - 0
public/index.html

@@ -23,6 +23,7 @@
         <script src="<%= BASE_URL %><%= VUE_APP_STATIC_DIR %>/static/lib/flv.min.js"></script>
         <script src="<%= BASE_URL %><%= VUE_APP_STATIC_DIR %>/static/lib/vconsole.js"></script>
         <script src="<%= BASE_URL %><%= VUE_APP_STATIC_DIR %>/static/lib/swiper/swiper-bundle.min.js"></script>
+        <script src="<%= BASE_URL %><%= VUE_APP_STATIC_DIR %>/static/lib/socket.io.min.js"></script>
 
 
         <script src="<%= BASE_URL %><%= VUE_APP_STATIC_DIR %>/static/lib/jweixin-1.6.0.js"></script>

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 2060 - 0
public/viewer/static/lib/socket.io.min.js


+ 2 - 2
src/apis/index.js

@@ -31,9 +31,9 @@ export const inCat = (data) => {
 
 export const burying_point = (data) => {
   let url = encodeURI(window.location.href)
-  return http.get("/api/Point", {
+  return http.get("/api/point", {
     userId: browser.getURLParam("userId") || 1,
-    accessToken: browser.getURLParam("accessToken")||'none',
+    AccessToken: browser.getURLParam("AccessToken")||'none',
     cookieId: browser.getURLParam("cookieId")||'none',
     isMiniApp: browser.getURLParam("isMiniApp")||'h5',
     whereUrl: url,

+ 16 - 4
src/app.vue

@@ -6,7 +6,7 @@
   <div class="ui-view-layout" :class="{ show: show }" is-mobile="true">
     <div class="scene" ref="scene$"></div>
     <template v-if="dataLoaded">
-      <Information />
+      <Information v-if="!isshoppingguide" />
       <Control />
       <teleport v-if="refMiniMap && player.showWidgets" :to="refMiniMap">
         <span class="button-switch" @click.stop="toggleMap">
@@ -23,7 +23,7 @@
           <div class="tabs" v-if="controls.showMap">
             <span :class="{ active: mode === 'floorplan' }" @click="changeMode('floorplan', $event)">
               <ui-icon :type="mode == 'floorplan' ? 'show_plane_selected' : 'show_plane_normal'"></ui-icon>
-              二維
+              二維 
             </span>
             <span :class="{ active: mode === 'dollhouse' }" @click="changeMode('dollhouse', $event)">
               <ui-icon :type="mode == 'dollhouse' ? 'show_3d_selected' : 'show_3d_normal'"></ui-icon>
@@ -65,6 +65,7 @@ import common from "@/utils/common";
 import { Cache } from "@/utils/index";
 
 import * as apis from "@/apis/index.js";
+const store = useStore();
 
 let jumpNewScene = (sceneFirstView) => {
   let url = window.location.href;
@@ -104,10 +105,12 @@ const closetagtype = () => {
   });
 };
 
-const store = useStore();
 const tags = computed(() => {
   return store.getters["tag/tags"] || [];
 });
+
+const isshoppingguide = computed(() => store.getters["shoppingguide"]);
+
 const player = computed(() => store.getters["player"]);
 const flying = computed(() => store.getters["flying"]);
 const metadata = computed(() => store.getters["scene/metadata"]);
@@ -229,6 +232,8 @@ const onClickTagInfo = (el) => {
   }
 };
 
+
+
 onMounted(async () => {
   apis.burying_point({ type: 0 });
 
@@ -295,7 +300,7 @@ onMounted(async () => {
         view.focus(tag.sid).then(() => {
           if (tag.type == "coupon") {
             try {
-              document.querySelector(`[data-tag-id="${tag.sid}"] .tag-icon`).style.display = 'none'
+              document.querySelector(`[data-tag-id="${tag.sid}"] .tag-icon`).style.display = "none";
               let hotcontent = JSON.parse(tag.hotContent);
               browser.openLink(
                 "/subPackage/pages/activity/activity?pageId=" + hotcontent.couponLink,
@@ -425,6 +430,13 @@ onMounted(async () => {
   });
   app.store.on("floorcad", (floor) => store.commit("scene/loadFloorData", floor));
 
+  app.Connect.follow.on('data', data => {
+    console.log(data,'syncdata');
+      // if (isJoined) {
+      //     socket.emit('sync', data)
+      // }
+  })
+
   app.render();
   document.removeEventListener("visibilitychange", visibilitychangeFn);
   document.addEventListener("visibilitychange", visibilitychangeFn);

BIN
src/assets/images/rtcLive/Input_disabled@2x.png


BIN
src/assets/images/rtcLive/Input_norma@2x.png


BIN
src/assets/images/rtcLive/arrows@2x.png


BIN
src/assets/images/rtcLive/avatar_small@2x.png


BIN
src/assets/images/rtcLive/brushes@2x.png


BIN
src/assets/images/rtcLive/brushes_selected@2_1.png


BIN
src/assets/images/rtcLive/brushes_selected@2x.png


BIN
src/assets/images/rtcLive/chat_off@2x.png


BIN
src/assets/images/rtcLive/chat_on@2x.png


BIN
src/assets/images/rtcLive/chrome.png


BIN
src/assets/images/rtcLive/cross@2x.png


BIN
src/assets/images/rtcLive/edge.png


BIN
src/assets/images/rtcLive/exit@2x.png


BIN
src/assets/images/rtcLive/firefox.png


BIN
src/assets/images/rtcLive/guided@2x.png


BIN
src/assets/images/rtcLive/hot_spot@2x.png


BIN
src/assets/images/rtcLive/hot_spot_selected@2x.png


BIN
src/assets/images/rtcLive/invitation@2x.png


BIN
src/assets/images/rtcLive/loading@2x.png


BIN
src/assets/images/rtcLive/members@2x.png


BIN
src/assets/images/rtcLive/mic_all_off@2x.png


BIN
src/assets/images/rtcLive/mic_all_on@2x.png


BIN
src/assets/images/rtcLive/mic_off@2x.png


BIN
src/assets/images/rtcLive/mic_off_50@2x.png


BIN
src/assets/images/rtcLive/mic_off_black@2x.png


BIN
src/assets/images/rtcLive/mic_on@2x.png


BIN
src/assets/images/rtcLive/pop-up_screen_off@2x.png


BIN
src/assets/images/rtcLive/pop-up_screen_on@2x.png


BIN
src/assets/images/rtcLive/remove@2x.png


BIN
src/assets/images/rtcLive/revocation@2x.png


BIN
src/assets/images/rtcLive/safari.png


BIN
src/assets/images/rtcLive/scene_mic_off@2x.png


BIN
src/assets/images/rtcLive/scene_mic_on@2x.png


BIN
src/assets/images/rtcLive/send@2x.png


BIN
src/assets/images/rtcLive/send_norma@2x.png


BIN
src/assets/images/rtcLive/send_selected@2x.png


BIN
src/assets/images/rtcLive/showing@2x.png


BIN
src/assets/images/rtcLive/speed.png


BIN
src/assets/images/rtcLive/video_off@2x.png


BIN
src/assets/images/rtcLive/video_off_50@2x.png


BIN
src/assets/images/rtcLive/video_on@2x.png


BIN
src/assets/images/rtcLive/vr@2x.png


+ 10 - 2
src/components/Controls/Control.Mobile.vue

@@ -3,6 +3,8 @@
   <transition mode="out-in">
       <component class="limitwidth" :is="panelPage"></component>
   </transition>
+
+
 </template>
 
 <script setup>
@@ -14,10 +16,15 @@ import FloorSwitch from "./FloorSwitch";
 import vMain from "./Panel/Main";
 import Guide from "./Panel/Guide";
 
+import guideShop from "../RTC/index";
+
+
 import { Scrollbar, Dialog } from "@/global_components/";
-const store = useStore();
 import common from "@/utils/common";
 import { useMusicPlayer } from "@/utils/sound";
+
+const store = useStore();
+
 const musicPlayer = useMusicPlayer();
 const metadata = computed(() => store.getters["scene/metadata"]);
 const flying = computed(() => store.getters["flying"]);
@@ -26,11 +33,12 @@ const controls = computed(() => {
 });
 const player = computed(() => store.getters["player"]);
 const mode = computed(() => store.getters["mode"]);
+
 let timer = null;
 
 const panelPage = computed(() => {
   let status = store.getters["tour/isPlay"] ? Guide :  vMain;
-  return status;
+  return store.getters["shoppingguide"] ? guideShop : status;
 });
 
 

+ 5 - 1
src/components/Controls/Panel/Main.vue

@@ -142,6 +142,7 @@ const menulist = computed(() => {
     },
     {
       icon: "guided_shopping",
+      id: "guided_shopping",
       name: "導購",
     },
     {
@@ -256,7 +257,10 @@ const onClickMenu = (item) => {
     "/subPackage/pages/shoppingcart/shoppingcart");
   } else if (item.id == "help") {
     store.commit("showUserGuide", true);
-  }
+  } else if (item.id == "guided_shopping") {
+    store.commit("showShoppingguide", true);
+  } 
+  
 };
 
 const getCategorylist = async () => {

+ 334 - 0
src/components/RTC/PageHome.vue

@@ -0,0 +1,334 @@
+<template>
+  <div class="navigation" v-if="orgin != 'fashilong' && !hideToolBar" :style="{ zIndex: showEnvTips ? 99999 : 99,left: !showMore?-iconListWidth+'px':''}">
+
+    <!-- 带看导航栏 -->
+    <div class="tabBar" v-if="tabBarShow">
+      <div class="iconList" id="iconList">
+        <!-- <div class="iconfont " :class="i.icon" v-for="i,index in tabbarList" :key="index" @click="tabBarClick(i.type)">
+          <div v-if="tabBarType==3 && showTabbarTip &&i.type==3" class="iconTip">热点</div>
+        </div> -->
+
+        <div v-if="plays.length>0" class="iconfont " :class="[this.status.isPlaying ? 'iconscene_stop' : 'iconguided']" :style="status.isPlaying ? 'color:#ED5D18;' : ''" @click="tabBarClick(1)">
+          <div v-if="tabBarType==1 && showTabbarTip && status.isPlaying  " class="iconTip">{{$t('rtcLive.openGuided')}}</div>
+          <div v-if="tabBarType==1 && showTabbarTip &&!status.isPlaying  " class="iconTip">{{$t('rtcLive.closeGuided')}}</div>
+        </div>
+        <div class="iconfont iconshow" @click="tabBarClick(2)">
+          <!-- <div v-if="tabBarType==2 && showTabbarTip  " class="iconTip">{{$t('rtcLive.create_VR')}}</div> -->
+        </div>
+        <div class="iconfont iconhot_spot" :style="hotSpot ? 'color:#ED5D18;' : ''" @click="tabBarClick(3)">
+          <div v-if="tabBarType==3 && showTabbarTip && hotSpot " class="iconTip">{{$t('rtcLive.openHotSpot')}}</div>
+          <div v-if="tabBarType==3 && showTabbarTip &&!hotSpot " class="iconTip">{{$t('rtcLive.closeHotSpot')}}</div>
+          <!-- <div class="iconTip">热点</div> -->
+        </div>
+        <div class="iconfont iconvr" @click="tabBarClick(4)">
+        </div>
+
+      </div>
+      <div @click="onShowMore" :class="{'hide':!showMore}" class="tabBtn iconfont iconarrows"></div>
+    </div>
+  </div>
+</template>
+<script>
+import config from "@/config";
+import XWindow from "../shared/XWindow";
+import { mapGetters } from "vuex";
+import { play } from "@/core";
+import {
+  backgroundMusicPlayer,
+  guideSoundPlayer,
+  checkReadyToPlay,
+} from "@/utils/sounds";
+import { sendToApp, userId } from "../../socket";
+import browser from "@/utils/browser";
+import { objects } from "@/core/base";
+
+export default {
+  components: {
+    XWindow,
+  },
+  data() {
+    return {
+      tabBarShow: true,
+      orgin: browser.urlQueryValue("origin").toLowerCase(),
+      showMore: true,
+      hideToolBar: browser.urlHasValue("hideToolBar"),
+      showCoupon: false,
+      showEnvTips: false,
+      iconListWidth: 0,
+      hotSpot: false,
+      tabbarList: [
+        {
+          title: "导览",
+          icon: "iconguided",
+          type: 1,
+        },
+        {
+          title: "一起逛",
+          icon: "iconshowing",
+          type: 2,
+        },
+        {
+          title: "热点",
+          icon: "iconhot_spot",
+          type: 3,
+        },
+        {
+          title: "VR模式",
+          icon: "iconvr",
+          type: 4,
+        },
+      ],
+      tabBarType: 0,
+      showTabbarTip: false,
+      timer: null,
+    };
+  },
+  watch: {
+    plays: {
+      handler: function (newVal, oldVal) {
+        sendToApp("guide", { length: this.plays.length });
+      },
+      immediate: true,
+    },
+    "status.isPlaying": function () {
+      sendToApp("guide", { playing: this.status.isPlaying });
+    },
+  },
+  computed: {
+    ...mapGetters({
+      plays: "guide/plays",
+      status: "guide/status",
+      medias: "guide/medias",
+    }),
+  },
+  created() {
+    this.$once("hook:beforeDestroy", () => {
+      this.$bus.off("vrhouse/back", this.vrBack);
+    });
+    this.$bus.on("vrhouse/back", this.vrBack);
+    play.on("guide/play/start", (index) => {
+      this.$store.commit("guide/SetStatus", {
+        isPlaying: true,
+      });
+    });
+
+    play.on("guide/play/pause", (index) => {
+      this.$store.commit("guide/SetStatus", {
+        isPlaying: false,
+      });
+    });
+
+    play.on("guide/play/stop", () => {
+      this.$store.commit("guide/SetStatus", {
+        isPlaying: false,
+      });
+    });
+
+    play.on("guide/play/playing", (index) => {
+      this.$store.commit("guide/SetIndex", index);
+    });
+
+    play.on("guide/play/flyToStart", (index) => {
+      this.$store.commit("guide/SetIndex", index);
+    });
+
+    // this.$bus.on("shop/app/action", (data) => {
+    //   if (data.type == "showCoupon") {
+    //     this.showCoupon = data.data;
+    //   } else if (data.type == "guide") {
+    //     if (this.status.isPlaying) {
+    //       play.pause();
+    //     } else {
+    //       play.start();
+    //     }
+    //   }
+    // });
+  },
+  mounted() {
+    this.$bus.on("ready", () => {
+      if (this.plays.length) {
+        let type =
+          this.status.audio === "soundsync" ? "sync" : this.status.audio;
+
+        if (this.medias.links[type]) {
+          guideSoundPlayer.setSRC(
+            this.$config.getPublishResource(this.medias.links[type])
+          );
+        }
+      }
+    });
+    this.$nextTick(() => {
+      $("#player").on("touchstart", () => {
+        if (this.status.isPlaying) {
+          play.pause();
+        }
+      });
+    });
+    // if (this.$config.appenv != "discover") {
+    //   sendToApp("cart", true);
+    // }
+    // sendToApp("ready", this.$config.projectNum);
+  },
+  methods: {
+    vrBack() {
+      this.tabBarShow = true;
+      this.$store.commit("SetVR", false);
+      this.$nextTick(() => {
+        if (this.hotSpot) {
+          objects.tagManager.showAllTags();
+        } else {
+          objects.tagManager.hideAllTags();
+        }
+      });
+    },
+    tabBarClick(type) {
+      this.tabBarType = type;
+      switch (type) {
+        case 1:
+          this.onPlayGuide();
+          break;
+        case 2:
+          //发起带看
+          play.pause();
+          this.$parent.showCreated = true;
+          this.$config.isTyping = true;
+          break;
+
+        case 3:
+          //热点
+          // play.pause();
+
+          this.hotSpot = !this.hotSpot;
+          if (this.hotSpot) {
+            objects.tagManager.showAllTags();
+          } else {
+            objects.tagManager.hideAllTags();
+          }
+          break;
+
+        case 4:
+          //VR模式
+          play.pause();
+          this.tabBarShow = false;
+          this.$bus.emit("vrhouse/enterVR");
+          this.$store.commit("SetVR", true);
+          this.$bus.emit("shop/header/showHeader", false);
+          break;
+
+        default:
+          break;
+      }
+      this.showTabbarTip = true;
+      this.timer = setTimeout(() => {
+        this.showTabbarTip = false;
+        clearTimeout(this.timer);
+        this.timer = null;
+      }, 3000);
+    },
+    onShowMore() {
+      this.iconListWidth = document.getElementById("iconList").clientWidth;
+      this.showMore = !this.showMore;
+    },
+    onPlayGuide() {
+      if (this.status.isPlaying) {
+        play.pause();
+      } else {
+        play.start();
+      }
+    },
+  },
+};
+</script>
+<style lang="scss" scoped>
+.navigation {
+  pointer-events: none;
+  position: absolute;
+  //   left: 0.5rem;
+  //   right: 0.5rem;
+  //   bottom: 0.8rem;
+  left: 0;
+  bottom: 1.2rem;
+  height: 1.3rem;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 100;
+  > div {
+    pointer-events: all;
+    height: 100%;
+  }
+  transition: all 0.3s;
+
+  &.hide {
+    left: -50%;
+  }
+}
+.tabBar {
+  //   min-width: 5.53rem;
+  height: 1.22rem;
+  background: rgba(0, 0, 0, 0.5);
+  border-radius: 0px 0.67rem 0.67rem 0px;
+  padding: 0 0.69rem 0 0.28rem;
+  .iconList {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
+    .iconfont {
+      //   width: 0.56rem;
+      //   height: 0.56rem;
+      // font-size: 0.56rem;
+      font-size: 0.65rem;
+      //   background: #fc6970;
+      margin: 0 0.35rem;
+      position: relative;
+      .iconTip {
+        height: 0.83rem;
+        background: rgba(0, 0, 0, 0.7);
+        padding: 0 0.17rem;
+        font-size: 0.28rem;
+        position: absolute;
+        top: -1.6rem;
+        left: 50%;
+        transform: translateX(-50%);
+        white-space: nowrap;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        color: #fff;
+        border-radius: 2px 2px 0 0;
+        &::after {
+          position: absolute;
+          content: "";
+          width: 0;
+          height: 0;
+          border-right: 0.28rem solid transparent;
+          border-left: 0.28rem solid transparent;
+          border-top: 0.28rem solid rgba(0, 0, 0, 0.7);
+          bottom: -0.28rem;
+          left: 50%;
+          transform: translateX(-50%);
+        }
+      }
+    }
+  }
+  .tabBtn {
+    // width: 0.42rem;
+    // height: 0.42rem;
+    font-size: 0.31rem;
+    position: absolute;
+    right: 0.4rem;
+    top: 50%;
+    transform: translateY(-50%);
+    transition: all 0.3s;
+    &.hide {
+      position: absolute;
+      right: 0.27rem;
+      top: 50%;
+      transform: translateY(-50%) rotate(180deg);
+    }
+  }
+}
+</style>

+ 928 - 0
src/components/RTC/PageRtcLive.vue

@@ -0,0 +1,928 @@
+<template>
+  <div id="PageRtcLive">
+    <div class="member_number">
+      <div class="members"></div>
+      <span>2观看</span>
+    </div>
+    <!-- <chat ref="chat" v-show="chatShow" :chatList="chatList" :user_info="user_info"></chat> -->
+    <div class="videoBox userVideo" id="userVideo" v-show="!hideVideoTop">
+      <img v-if="!userVideoShow" :src="require('@/assets/images/rtcLive/avatar_small@2x.png')" alt="" />
+      <img v-if="!userVideoShow" class="loadingTip" :src="require('@/assets/images/rtcLive/loading@2x.png')" alt="" />
+      <div class="micBox">
+        <i v-if="hideMicTop" class="iconfont iconscene_mic_off1"></i>
+        <i v-else class="speak_mic"></i>
+      </div>
+    </div>
+    <div v-show="mode == '1' && !hideVideoBottom" class="videoBox myVideo" id="myVideo">
+      <img v-if="!myVideoShow" :src="require('@/assets/images/rtcLive/avatar_small@2x.png')" alt="" />
+      <img v-if="!myVideoShow" class="loadingTip" :src="require('@/assets/images/rtcLive/loading@2x.png')" alt="" />
+      <div class="micBox">
+        <i v-if="hideMic" class="iconfont iconscene_mic_off1"></i>
+        <i v-else class="speak_mic"></i>
+      </div>
+    </div>
+    <!-- </div> -->
+    <!-- <input type="text" autocomplete="off"> -->
+    <div class="contorlBar" v-if="!showInput">
+      <div v-if="connectStatus == 1" :class="{ disable: !user_info.isWords }" class="saySomething" @click="onFocus">
+        <!-- <i class="speakIcon"
+           :class="{'dis':!user_info.isWords}"></i> -->
+        <span v-if="user_info.isWords"> 说点什么 </span>
+        <span v-if="!user_info.isWords">已被禁言</span>
+
+        <div class="disSpeakBtn" @click.stop="chatShow = !chatShow" :class="{ dis: !chatShow }"></div>
+      </div>
+      <div style="text-align: right; width: 100%" v-if="connectStatus == 0">连接中...</div>
+      <div v-if="connectStatus == 1" class="contorl_btn">
+        <div v-if="isBrushes" @click="onDrawUndo" class="brushesBack" :class="{ disable: !canUndo }"></div>
+        <div v-if="user_info.Role == 'leader'" @click="onDraw" :class="{ brushesed: isBrushes }" class="brushes"></div>
+
+        <div
+          v-if="(user_list.length < 2 && mode == '1') || (mode == '2' && user_list.length < 30)"
+          class="invitation"
+          @click="openDialog('dialogShare', shareLink)"
+        ></div>
+        <div v-if="mode == '2' && role == 'leader'" class="members" @click="openMember"></div>
+        <div v-if="audioDevices.length != 0 && !disableMic" @click="changeMedia('audio', hideMic)" :class="{ mic_off: hideMic }" class="mic_on"></div>
+        <div v-if="audioDevices.length == 0 || disableMic" class="mic_no"></div>
+        <div
+          v-if="(role == 'leader' && videoDevices.length != 0) || (mode == '1' && videoDevices.length != 0)"
+          @click="changeMedia('video', hideVideo)"
+          :class="{ video_off: hideVideo }"
+          class="video_on"
+        ></div>
+        <div v-if="(videoDevices.length == 0 && mode == '1') || (role == 'leader' && videoDevices.length == 0)" class="video_no"></div>
+        <div class="exit" @click="openDialog('dialogIndex')"></div>
+      </div>
+    </div>
+    <div class="layer" v-if="showInput" @click="closeInput">
+      <div class="inputBox" @click.stop>
+        <div class="msgBox">
+          <input id="input_msg" type="text" maxlength="200" v-model.trim="text" :placeholder="`说点什么~`" />
+          <span class="iconsend_icon" :class="{ disable: text == '' }" @click.stop="sendText">发送</span>
+        </div>
+      </div>
+    </div>
+
+    <!-- 成员 -->
+    <div class="layer" v-if="showMember" @click.self="closeMember">
+      <div class="memberContent animated" :class="animateActive ? 'fadeInUpBig' : 'fadeOutDownBig'">
+        <div class="blurBox"></div>
+        <div class="content">
+          <div class="memberHeader">
+            <span> 成员管理({{ user_list.length }})</span>
+            <i class="iconfont"></i>
+          </div>
+          <div class="memberList">
+            <div class="memberItem">
+              <div class="userMsg">
+                <div class="avatar">
+                  <img :src="require('@/assets/images/rtcLive/avatar_small@2x.png')" alt="" />
+                </div>
+                <div class="name" v-if="user_info.Role == 'leader'">{{ user_info.Nickname }} (主持人,我)</div>
+                <div class="name" v-else>{{ user_info.Nickname }} (我)</div>
+              </div>
+
+              <div class="button" v-if="user_info.Role == 'leader'">
+                <!-- <div class="outBtn iconfont iconremove"></div> -->
+                <!-- <div v-if="audioDevices.length>0" class="micBtn iconfont" @click="changeMedia('audio',hideMic)" :class="user_info.muted ?'iconmic_off':'iconmic_on'"></div>
+                <div v-else class="micBtn iconfont iconmic_off"></div> -->
+                <div class="micBtn mute_all_mic" :class="{ open_all_mic: !all_mute_mic }" @click="onMemberMuted(all_mute_mic)"></div>
+              </div>
+            </div>
+
+            <div v-show="user_info.UserId != i.UserId && i.role != 'leader'" class="memberItem" v-for="i in user_list" :key="i.UserId">
+              <div class="userMsg">
+                <div class="avatar">
+                  <img :src="require('@/assets/images/rtcLive/avatar_small@2x.png')" alt="" />
+                </div>
+                <div class="name">{{ i.userName }}</div>
+              </div>
+              <div class="button" v-if="user_info.Role == 'leader'">
+                <div class="micBtn" :class="i.isWords ? 'ban_speak_on' : 'ban_speak_off'" @click="userCanSpeak(i)"></div>
+
+                <div class="outBtn icon_remove" @click="userGetOut(i.UserId)"></div>
+                <div class="micBtn" :class="i.isMuted ? 'mute_one_mic_off' : 'mute_one_mic_on'" @click="onMemberMuted(!i.isMuted, i.UserId)"></div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <Draw ref="draw" @sendCanUndo="sendCanUndo" :show-paint="showPaint" />
+  </div>
+</template>
+
+<script setup>
+import { onMounted, watch, defineEmits, ref, nextTick } from "vue";
+import { useApp, getApp } from "@/app";
+import { useStore } from "vuex";
+import { Scrollbar, Dialog } from "@/global_components/";
+
+// import Draw from './paint/Draw';
+import { mapGetters } from "vuex";
+// import chat from './chat/chat.vue';
+import browser from "@/utils/browser";
+
+const emit = defineEmits(["openDialog"]);
+
+const store = useStore();
+
+let createSocket = (config) => {
+  var socket = io("https://vr-admin.cdfmembers.com/", {
+    path: "/ws-sync",
+    transports: ["websocket"],
+  });
+  return socket;
+};
+
+let getUrl = (href, queryArr) => {
+  queryArr.forEach((item) => {
+    if (!browser.hasURLParam(item.key)) {
+      href += `&${item.key}=${item.val}`;
+    } else {
+      href = browser.replaceQueryString(href, item.key, item.val);
+    }
+  });
+
+  return href;
+};
+
+const connectStatus = ref(0);
+const showPaint = ref(true);
+const isBrushes = ref(false);
+const hideVideo = ref(false);
+const hideVideoBottom = ref(false);
+const hideVideoTop = ref(true);
+const hideMic = ref(false);
+const hideMicTop = ref(false);
+const showInput = ref(false);
+const showMember = ref(false);
+const animateActive = ref(false);
+const socket = ref(createSocket());
+const myVideoShow = ref(false);
+const userVideoShow = ref(false);
+const user_info = ref({});
+const user_list = ref([]);
+const mode = ref(browser.getURLParam("mode"));
+const role = ref(browser.getURLParam("role"));
+const chatList = ref([]);
+const text = ref("");
+
+const shareLink = ref("");
+const canUndo = ref(false);
+const audioDevices = ref([1]);
+const videoDevices = ref([1]);
+const disableMic = ref(false);
+const leaderInfo = ref(null);
+const chatShow = ref(true);
+const all_mute_mic = ref(true);
+
+const onDrawUndo = () => {};
+
+const setRtc = (res) => {
+  console.log("成功进入", res);
+  self.connectStatus = 1;
+
+  self.user_info = res.user;
+  self.user_list = res.users;
+  //分享链接
+  if (!(window.self === window.top)) {
+    // console.log(document.referrer)
+    var origin = "";
+    if (parent != window) {
+      try {
+        origin = parent.location.href;
+      } catch (e) {
+        // origin = document.referrer;
+
+        origin = window.name;
+      }
+    }
+    self.shareLink = origin + "#/sceneVisitor" + window.location.search + "#/?mode=" + self.mode + "&role=customer&roomId=" + self.user_info.RoomId;
+  } else {
+    self.shareLink =
+      window.location.origin + window.location.pathname + window.location.search + "#/?mode=" + self.mode + "&role=customer&roomId=" + self.user_info.RoomId;
+  }
+
+  let hash = "";
+
+  if (self.role == "leader") {
+    if (!browser.urlHashValue("roomId")) {
+      hash += "&roomId=" + self.user_info.RoomId;
+    }
+    if (!browser.urlHashValue("userId")) {
+      hash += "&userId=" + self.user_info.UserId;
+    }
+  } else {
+    if (!browser.urlHashValue("userId")) {
+      hash += "&userId=" + self.user_info.UserId;
+    }
+  }
+  //重定向hash
+  window.location.replace(window.location.href + hash);
+  if (!(window.self === window.top)) {
+    //判断在iframe内 需要告知外层
+    window.parent.postMessage({ type: "msg", url: window.location.href + hash }, "*");
+  }
+  //创建通话
+  if (!checkBrowserSupport()) {
+    self.$parent.showCheckBrowser = true;
+  } else {
+    checkDevices()
+      .then((res) => {
+        self.audioDevices = res.filter(function (device) {
+          return device.kind === "audioinput";
+        });
+        self.videoDevices = res.filter(function (device) {
+          return device.kind === "videoinput";
+        });
+        console.log("audioDevices", self.audioDevices);
+        console.log("videoDevices", self.videoDevices);
+        initRtc(self.user_info.RoomId, {
+          videoDevices: self.videoDevices,
+          audioDevices: self.audioDevices,
+        });
+      })
+      .catch((err) => {
+        console.log(err);
+        initRtc(self.user_info.RoomId, {
+          videoDevices: self.videoDevices,
+          audioDevices: self.audioDevices,
+        });
+      });
+  }
+};
+
+const onFocus = () => {
+  showInput.value = true;
+  nextTick(() => {
+    let input_msg = document.getElementById("input_msg");
+    input_msg.focus();
+  });
+};
+
+// 关闭输入框
+const closeInput = () => {
+  showInput.value = false;
+  text.value = "";
+};
+
+// 开启成员
+const openMember = () => {
+  showMember.value = true;
+  animateActive.value = true;
+};
+// 关闭成员
+const closeMember = () => {
+  animateActive.value = false;
+  let t = setTimeout(() => {
+    clearTimeout(t);
+    showMember.value = false;
+  }, 200);
+};
+
+const openDialog = (str, link) => {
+  // getApp().Connect.follow.start();
+  emit("openDialog", str, link);
+};
+
+const onMemberMuted = (muted, userId) => {
+  if (user_list.value.length > 1) {
+    Dialog.toast({ content: `操作成功`, type: "success" });
+    socket.value && socket.value.send("Muted", muted, userId);
+  }
+};
+
+const userCanSpeak = (item) => {
+  item.isWords = !item.isWords;
+  socket.value && socket.value.invoke("Words", isWords, userId);
+};
+
+const startFollow = (app) => {
+  var isJoined = false;
+
+  socket.value.on("connect", () => {
+    console.log(socket.value, "socket");
+    socket.value.emit("join", {
+      userId: Date.now().toString(),
+      roomId: "test-room",
+      role: "leader",
+    });
+  });
+
+  socket.value.on("action", (data) => {
+    if (data.type == "error") {
+    } else if (data.type == "user-join") {
+
+      connectStatus.value = 1
+      user_info.value = data.user;
+      user_list.value = data.members;
+
+
+
+      console.log(user_info.value, "user_info");
+
+      shareLink.value = getUrl(window.location.href, [
+        {
+          key: "mode",
+          val: mode.value,
+        },
+        {
+          key: "name",
+          val: "",
+        },
+        {
+          key: "role",
+          val: "customer",
+        },
+        {
+          key: "roomId",
+          val: user_info.value.RoomId,
+        },
+      ]);
+
+      isJoined = true;
+    }
+  });
+
+  socket.value.on("sync", (data) => {
+    app.Connect.sync.receive(data);
+  });
+};
+
+onMounted(() => {
+  useApp().then((app) => {
+    startFollow(app);
+  });
+});
+</script>
+
+<style scoped lang="scss">
+#PageRtcLive {
+  position: absolute;
+  left: 0;
+  bottom: 0;
+  // height: 7.31rem;
+  width: 100%;
+  z-index: 999;
+  //   background: rgba(0, 0, 0, 0.1);
+  padding: 0 0.44rem;
+  box-sizing: border-box;
+  // pointer-events: none;
+  .member_number {
+    // width: 1.67rem;
+    height: 0.5rem;
+    background: rgba(0, 0, 0, 0.3);
+    border-radius: 0.25rem;
+    position: fixed;
+    top: 0.56rem;
+    right: 0.44rem;
+    padding: 0.07rem 0.17rem;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    .members {
+      width: 0.42rem;
+      height: 0.42rem;
+      background: url(~@/assets/images/rtcLive/members@2x.png);
+      background-size: 100% 100%;
+      margin-right: 0.07rem;
+    }
+    span {
+      font-size: 0.24rem;
+      color: #fff;
+    }
+  }
+  .videoBox {
+    width: 1.94rem;
+    height: 1.94rem;
+    border-radius: 50%;
+    overflow: hidden;
+    position: fixed;
+    .loadingTip {
+      position: absolute;
+      z-index: 101;
+      left: 0;
+      top: 0;
+      animation: Rotate 1.5s infinite;
+      @keyframes Rotate {
+        0% {
+          transform: rotate(0deg);
+        }
+        100% {
+          transform: rotate(360deg);
+        }
+      }
+    }
+    > img {
+      width: 1.94rem;
+      height: 1.94rem;
+    }
+    .videoPlayer {
+      width: 1.94rem;
+      height: 1.94rem;
+      position: relative;
+      z-index: 2;
+    }
+    &.userVideo {
+      top: 0.69rem;
+      left: 0.53rem;
+      // background-size: ;
+    }
+    &.myVideo {
+      bottom: 2.72rem;
+      right: 0.69rem;
+    }
+
+    .micBox {
+      width: 100%;
+      height: 0.44rem;
+      background: rgba(0, 0, 0, 0.3);
+      position: absolute;
+      left: 0;
+      bottom: 0;
+      z-index: 100;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      .speak_mic {
+        display: block;
+        background-size: 13.3344rem auto;
+        width: 0.22rem;
+        height: 0.22rem;
+        background-image: url(~@/assets/images/rtcLive/speed.png);
+        // width: 0.69rem;
+        // height: 0.69rem;
+        animation: myAnimation 3s steps(59) infinite;
+      }
+      .iconscene_mic_off1 {
+        font-size: 0.22rem;
+        color: #f56c6c;
+      }
+    }
+  }
+
+  .contorlBar {
+    width: 9.51rem;
+    height: 1.173333rem;
+    background: rgba(0, 0, 0, 0.5);
+    border-radius: 0.67rem;
+    border: 0.03rem solid rgba(255, 255, 255, 0.1);
+    margin: 0.67rem auto 0;
+    // position: absolute;
+    position: fixed;
+    z-index: 9999;
+    left: 50%;
+    transform: translateX(-50%);
+    padding: 0 0.266667rem;
+    box-sizing: border-box;
+    bottom: 0.94rem;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    .saySomething {
+      color: #fff;
+      // font-size: 0.42rem;
+      height: 0.8rem;
+      position: relative;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      background: rgba(0, 0, 0, 0.5);
+      padding: 0 0.2rem 0 0.2rem;
+      border-radius: 0.64rem;
+      width: 100%;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      .disSpeakBtn {
+        width: 0.533333rem;
+        height: 0.533333rem;
+        background: url(~@/assets/images/rtcLive/pop-up_screen_on@2x.png) no-repeat;
+        background-size: 100% 100%;
+        cursor: pointer;
+        &.dis {
+          background: url(~@/assets/images/rtcLive/pop-up_screen_off@2x.png) no-repeat;
+          background-size: 100% 100%;
+        }
+      }
+      .speakIcon {
+        width: 0.32rem;
+        height: 0.32rem;
+        background: url(~@/assets/images/rtcLive/Input_norma@2x.png);
+        background-size: 100% 100%;
+        display: block;
+        margin-right: 0.1rem;
+        &.dis {
+          background: url(~@/assets/images/rtcLive/Input_disabled@2x.png);
+          background-size: 100% 100%;
+        }
+      }
+      span {
+        font-size: 0.266667rem;
+      }
+      // &::after {
+      //   content: "";
+      //   position: absolute;
+      //   width: 0.04rem;
+      //   height: 0.44rem;
+      //   background: #ffffff;
+      //   border-radius: 0.03rem;
+      //   top: 50%;
+      //   transform: translateY(-50%);
+      //   right: -0.28rem;
+      // }
+    }
+    .contorl_btn {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      margin-left: 0.2rem;
+      > div {
+        // width: 0.56rem;
+        // height: 0.56rem;
+        width: 0.65rem;
+        height: 0.65rem;
+        // font-size: 0.56rem;
+        // background: #FD5151;
+        margin-right: 0.33rem;
+        // &.iconexit {
+        //   //   width: 0.56rem;
+        //   //   height: 0.56rem;
+        //   color: #fd5151;
+        //   //   background: #fff;
+        //   //   border-radius: 50%;
+        //   //   overflow: hidden;
+        // }
+        &.brushesBack {
+          background: url(~@/assets/images/rtcLive/revocation@2x.png);
+          background-size: 100% 100%;
+        }
+        &.brushes {
+          // background: url(~@/assets/images/rtcLive/brushes@2x.png);
+          background: url(~@/assets/images/rtcLive/brushes@2x.png);
+          background-size: 100% 100%;
+        }
+        &.brushesed {
+          background: url(~@/assets/images/rtcLive/brushes_selected@2_1.png);
+          background-size: 100% 100%;
+        }
+        &.invitation {
+          background: url(~@/assets/images/rtcLive/invitation@2x.png);
+          background-size: 100% 100%;
+        }
+        &.members {
+          background: url(~@/assets/images/rtcLive/members@2x.png);
+          background-size: 100% 100%;
+        }
+        &.mic_on {
+          background: url(~@/assets/images/rtcLive/mic_on@2x.png);
+          background-size: 100% 100%;
+        }
+        &.mic_no {
+          background: url(~@/assets/images/rtcLive/mic_off_50@2x.png);
+          background-size: 100% 100%;
+        }
+        &.mic_off {
+          background: url(~@/assets/images/rtcLive/mic_off@2x.png);
+          background-size: 100% 100%;
+        }
+        &.video_on {
+          background: url(~@/assets/images/rtcLive/video_on@2x.png);
+          background-size: 100% 100%;
+        }
+        &.video_no {
+          background: url(~@/assets/images/rtcLive/video_off_50@2x.png);
+          background-size: 100% 100%;
+        }
+        &.video_off {
+          background: url(~@/assets/images/rtcLive/video_off@2x.png);
+          background-size: 100% 100%;
+        }
+        &.exit {
+          background: url(~@/assets/images/rtcLive/exit@2x.png);
+          background-size: 100% 100%;
+        }
+        &:last-child {
+          margin-right: 0;
+        }
+      }
+    }
+  }
+  .layer {
+    width: 100vw;
+    height: 100vh;
+    background: rgba(0, 0, 0, 0.1);
+    z-index: 10000;
+
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    // right: unset;
+    // top: unset;
+    .inputBox {
+      width: 100vw;
+      height: 1.39rem;
+      // background: #f2f2f2;
+      border: 1px solid rgba(255, 255, 255, 0.1);
+      background: rgba(0, 0, 0, 0.5);
+
+      position: absolute;
+      bottom: 0;
+      left: 0;
+      padding: 0 0.44rem;
+      box-sizing: border-box;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      .msgBox {
+        width: 9.53rem;
+        height: 0.94rem;
+        position: absolute;
+        > input {
+          width: 9.53rem;
+          height: 0.94rem;
+          background: rgba(0, 0, 0, 0.5);
+          border-radius: 0.56rem;
+          font-size: 0.42rem;
+          padding: 0 1.6rem 0 0.44rem;
+          box-sizing: border-box;
+          border: none;
+          outline: none;
+          color: #fff;
+          resize: none;
+          line &::placeholder {
+            color: rgba(255, 255, 255, 0.5);
+          }
+        }
+        .iconfont {
+          // width: 0.56rem;
+          // height: 0.56rem;
+          // background: #ed5d18;
+          font-size: 0.56rem;
+          position: absolute;
+          top: 50%;
+          transform: translateY(-50%);
+          right: 0.78rem;
+          color: #fff;
+          &.active {
+            color: #ed5d18;
+          }
+        }
+        .iconsend_icon {
+          position: absolute;
+          top: 50%;
+          transform: translateY(-50%);
+          // right: 0.78rem;
+          right: 0.106667rem;
+          width: 1.28rem;
+          height: 0.693333rem;
+          background: #ed5d18;
+          color: #fff;
+          font-size: 0.32rem;
+          text-align: center;
+          line-height: 0.693333rem;
+          border-radius: 0.533333rem;
+          // width: 0.67rem;
+          // height: 0.67rem;
+          // background-image: url(~@/assets/images/rtcLive/send_selected@2x.png);
+          // background-size: 100% 100%;
+        }
+      }
+    }
+    .memberContent {
+      height: auto;
+      width: 100%;
+      // background: #ffffff;
+      border-radius: 0.28rem 0.28rem 0px 0px;
+      position: absolute;
+      left: 0;
+      bottom: 0;
+      &.animated {
+        animation-duration: 0.3s;
+      }
+      .blurBox {
+        background: rgba(0, 0, 0, 0.9);
+        border-radius: 0.14rem 0.14rem 0px 0px;
+        filter: blur(1px);
+        position: absolute;
+        width: 100%;
+        height: 100%;
+        z-index: 1;
+        top: 0;
+        left: 0;
+      }
+      .content {
+        position: relative;
+        width: 100%;
+        height: 100%;
+        z-index: 2;
+        top: 0;
+        left: 0;
+      }
+      .memberHeader {
+        width: 100%;
+        height: 1.28rem;
+        text-align: center;
+        line-height: 1.28rem;
+        > span {
+          font-size: 0.42rem;
+          color: #fff;
+        }
+      }
+      .memberList {
+        width: 100%;
+        height: 8.33rem;
+        border-top-style: solid;
+        border-top-color: rgba(0, 0, 0, 0.1);
+        border-top-width: 1px;
+        border-bottom-style: solid;
+        border-bottom-color: rgba(0, 0, 0, 0.1);
+        border-bottom-width: 1px;
+        box-sizing: border-box;
+        overflow-y: auto;
+        &::-webkit-scrollbar {
+          display: none;
+        }
+        .memberItem {
+          width: 100%;
+          height: 1.39rem;
+          display: flex;
+          align-items: center;
+          justify-content: space-between;
+          padding: 0.19rem 0.44rem;
+
+          .userMsg {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+
+            .avatar {
+              width: 1rem;
+              height: 1rem;
+              border-radius: 50%;
+              // overflow: hidden;
+              margin-right: 0.28rem;
+              > img {
+                // width: 100%;
+                // height: 100%;
+                width: 1rem;
+                height: 1rem;
+              }
+            }
+            .name {
+              font-size: 0.39rem;
+              color: #fff;
+            }
+          }
+          .button {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            > div {
+              //   width: 0.56rem;
+              //   height: 0.56rem;
+              //   background: #fc6970;
+              font-size: 0.65rem;
+              color: #fff;
+              &.micBtn {
+                width: 0.65rem;
+                height: 0.65rem;
+                margin-right: 0.3rem;
+                &.mute_all_mic {
+                  background-image: url(~@/assets/images/rtcLive/mic_all_on@2x.png);
+                  background-size: 100% 100%;
+                }
+                &.open_all_mic {
+                  background-image: url(~@/assets/images/rtcLive/mic_all_off@2x.png);
+                  background-size: 100% 100%;
+                }
+                &.mute_one_mic_on {
+                  background-image: url(~@/assets/images/rtcLive/mic_on@2x.png);
+                  background-size: 100% 100%;
+                }
+                &.mute_one_mic_off {
+                  background-image: url(~@/assets/images/rtcLive/mic_off@2x.png);
+                  background-size: 100% 100%;
+                }
+                &.ban_speak_on {
+                  background-image: url(~@/assets/images/rtcLive/chat_on@2x.png);
+                  background-size: 100% 100%;
+                }
+                &.ban_speak_off {
+                  background-image: url(~@/assets/images/rtcLive/chat_off@2x.png);
+                  background-size: 100% 100%;
+                }
+              }
+              &.outBtn {
+                margin-right: 0.3rem;
+                width: 0.65rem;
+                height: 0.65rem;
+                &.icon_remove {
+                  background-image: url(~@/assets/images/rtcLive/remove@2x.png);
+                  background-size: 100% 100%;
+                }
+              }
+            }
+          }
+        }
+      }
+      .memberBottom {
+        width: 100%;
+        padding: 0 0.44rem;
+        box-sizing: border-box;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        padding: 0.28rem 0;
+        box-sizing: border-box;
+        > div {
+          width: 4.61rem;
+          height: 1.11rem;
+          color: #ed5d18;
+          border-radius: 0.11rem;
+          border: 0.03rem solid #ed5d18;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          font-size: 0.39rem;
+          &.mute_all {
+            margin-right: 0.31rem;
+          }
+        }
+      }
+    }
+  }
+
+  .chat__area-layoutBox {
+    display: block;
+    width: 100%;
+    height: auto;
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    right: unset;
+    top: unset;
+    background: #fff;
+    // padding-bottom: 0.821256rem;
+    .chat__area-layoutBox_bg {
+      width: 100vw;
+      height: 100vh;
+      position: fixed;
+      z-index: 1;
+      top: 0;
+      left: 0;
+    }
+    .chat__area-l-content {
+      display: -webkit-box;
+      display: -ms-flexbox;
+      display: flex;
+      -webkit-box-orient: horizontal;
+      -webkit-box-direction: normal;
+      -ms-flex-direction: row;
+      flex-direction: row;
+      -webkit-box-align: center;
+      -ms-flex-align: center;
+      align-items: center;
+      -webkit-box-pack: center;
+      -ms-flex-pack: center;
+      justify-content: center;
+      min-height: 0.821256rem;
+      padding: 0.241546rem 0;
+      width: 100%;
+      position: relative;
+      z-index: 100;
+      textarea {
+        width: calc(100% - 0.96618rem);
+        -ms-flex-preferred-size: calc(100% - 0.96618rem);
+        flex-basis: calc(100% - 0.96618rem);
+        background: none;
+        border: none;
+        border-radius: 0;
+        color: #000;
+        font-size: 0.386473rem;
+        line-height: 0.483092rem;
+        padding: 0 0.241546rem;
+        resize: none;
+        -webkit-user-select: text;
+        caret-color: #ff4500;
+        overflow: hidden !important;
+      }
+    }
+  }
+
+  @keyframes myAnimation {
+    0% {
+      background-position: 0px 0px;
+      background-size: 13.3344rem auto;
+    }
+
+    100.00% {
+      background-position: -13.1104rem 0px;
+      background-size: 13.3344rem auto;
+    }
+  }
+}
+</style>

+ 640 - 0
src/components/RTC/PageShopping.vue

@@ -0,0 +1,640 @@
+<template>
+    <!-- <div class="navigation" v-if="orgin!='fashilong'"> -->
+    <div class="navigation">
+        <div class="wrapper sync" v-show="users.length">
+            <div>
+                <ul>
+                    <li
+                        v-for="(user, key) in users"
+                        :key="key"
+                        @click="user.userId == userId && onSetMic()"
+                        :class="[user.role]"
+                    >
+                        <img
+                            :src="
+                                user.avatar ||
+                                    require('images/vrhouse/avatar_default.jpg')
+                            "
+                            alt
+                        />
+                        <span class="voice" v-show="user.onlineStatus && user.voiceStatus != 1">
+                            <i
+                                class="iconfont"
+                                :class="[
+                                    user.voiceStatus == 2
+                                        ? 'iconscene_mic_open'
+                                        : 'iconscene_mic_off',
+                                ]"
+                            ></i>
+                        </span>
+                        <span class="status" v-show="!user.onlineStatus">
+                            <span> 已离开 </span>
+                        </span>
+                        <template v-if="user.onlineStatus">
+                            <div v-if="user.userId == userId">我</div>
+                            <div v-else-if="user.role == 'leader'">发起人</div>
+                        </template>
+                    </li>
+                    <li v-if="users.length < 5" class="add" @click="onInvite">
+                        <i class="iconfont icon_plus"></i>
+                    </li>
+                </ul>
+            </div>
+            <button @click="showExit = true">退出</button>
+        </div>
+        <div class="chat" ref="chat">
+            <transition-group appear tag="ul">
+                <li v-for="item in message" :key="item.id">
+                    <div>
+                        <div class="message">
+                            <img
+                                :src="
+                                    item.avatar ||
+                                        $config.getStaticResource(
+                                            'img/apps/vrhouse/avatar_default.jpg'
+                                        )
+                                "
+                                alt
+                            />
+                            <span v-if="item.nickname" v-html="`「${item.nickname}」`"></span>
+                            <span>{{ item.content }}</span>
+                        </div>
+                    </div>
+                </li>
+            </transition-group>
+        </div>
+        <x-window :show="showExit" :show-close="false">
+            <div class="exit-tips">
+                <h4>温馨提示</h4>
+                <div>确定要结束“一起逛”吗?</div>
+                <button type="button" @click="onExit(false)">继续逛</button>
+                <button type="submit" @click="onExit(true, true)">结束</button>
+            </div>
+        </x-window>
+        <x-window :show="showDissolve" :show-close="false">
+            <div class="exit-tips">
+                <h4>结束提醒</h4>
+                <div>
+                    发起人已结束一起逛模式,房间即
+                    <br />将关闭
+                </div>
+                <button type="button" @click="onExit(true, true)">确定</button>
+            </div>
+        </x-window>
+        <x-window :show="showFull" :show-close="false">
+            <div class="exit-tips">
+                <h4>温馨提示</h4>
+                <div>一起逛房间已满人,是否继续逛</div>
+                <button type="button" @click="onExit(true, true)">
+                    继续逛
+                </button>
+                <button type="submit" @click="onBack()">取消</button>
+            </div>
+        </x-window>
+        <x-window :show="showOver" :show-close="false">
+            <div class="exit-tips">
+                <h4>温馨提示</h4>
+                <div>一起逛已结束了,是否继续逛</div>
+                <button type="button" @click="onExit(true, true)">
+                    继续逛
+                </button>
+                <button type="submit" @click="onBack()">取消</button>
+            </div>
+        </x-window>
+        <Paint :show-paint="showPaint" />
+    </div>
+</template>
+<script>
+import { mapGetters } from "vuex"
+import XWindow from "../shared/XWindow"
+import browser from "@/utils/browser"
+import Paint from "./paint"
+import { startCall, stopCall, sendToApp, sendToH5, getRole, getUserId, close } from "../../socket"
+export default {
+    components: {
+        Paint,
+        XWindow,
+    },
+    data() {
+        return {
+            role: null,
+            userId: null,
+            users: [],
+            showExit: false,
+            showDissolve: false,
+            showOver: false,
+            showFull: false,
+            showCart: false,
+            showPaint: false,
+            goods: null,
+            chats: [],
+            orgin:browser.urlQueryValue('origin').toLowerCase(),
+        }
+    },
+    watch: {
+        users() {
+            if (!this.showCart) {
+                this.showCart = true
+                sendToApp("cart", true)
+            }
+        },
+        message() {
+            this.$nextTick(() => {
+                setTimeout(() => {
+                    // this.chats.shift()
+                }, 3000)
+            })
+            // this.$nextTick(() => {
+            //     this.message.length &&
+            //         this.$refs.chat
+            //             .querySelector("li:last-child")
+            //             .scrollIntoView({
+            //                 behavior: "smooth",
+            //                 block: "end",
+            //                 inline: "end",
+            //             });
+            // });
+        },
+    },
+    computed: {
+        ...mapGetters({
+            player: "player",
+        }),
+        message() {
+            return this.chats
+        },
+    },
+    created() {
+        if(browser.urlQueryValue('origin').toLowerCase() == '4dplaza'){
+            this.showPaint = true
+        }
+
+        this.role = getRole()
+        this.userId = getUserId()
+
+        const self = this
+        function syncAction(data) {
+            if (data.type == "full") {
+                self.isNoTips() && (self.showFull = true)
+                return
+            }
+            // 更新用户列表
+            if (data.type == "users") {
+                let leader
+                for (let i = 0; i < data.data.length; i++) {
+                    if (data.data[i].role == "leader") {
+                        leader = data.data.splice(i, 1)
+                        break
+                    }
+                }
+                if (leader) {
+                    data.data = leader.concat(data.data)
+                }
+                if (!data.data.some(item => item.role == "leader")) {
+                    // 房间已解散
+                    self.isNoTips() && (self.showOver = true)
+                    return
+                }
+                self.users = data.data
+            } else if (data.type == "userChange") {
+                let leader
+                for (let i = 0; i < data.data.length; i++) {
+                    if (data.data[i].role == "leader") {
+                        leader = data.data.splice(i, 1)
+                        break
+                    }
+                }
+                if (leader) {
+                    data.data = leader.concat(data.data)
+                }
+                self.users = data.data
+                if (self.role == "leader" && data.user.role != "leader") {
+                    self.showMessage(`${data.isOnline ? "进入" : "离开"}房间`, data.user)
+                } else if (self.role != "leader" && data.user.role == "leader") {
+                    data.user.nickname = "发起人"
+                    self.showMessage(`${data.isOnline ? "进入" : "离开"}房间`, data.user)
+                }
+            } else if (data.type == "online") {
+                if (self.role == "leader" && data.user.role != "leader") {
+                    self.showMessage(`${data.isOnline ? "进入" : "退出"}房间`, data.user)
+                }
+            }
+            // 解散房间
+            else if (data.type == "dissolve") {
+                self.isNoTips() && (self.showDissolve = true)
+            } else if (data.type == "close") {
+                self.isNoTips() && (self.showOver = true)
+            } else if (data.type == "tagShow") {
+                self.$bus.emit("shop/tag/show", data.data)
+            } else if (data.type == "tagSwitch") {
+                self.$bus.emit("shop/tag/switch", data.data)
+            } else if (data.type == "tagBigimg") {
+                self.$bus.emit("shop/tag/bigimg", data.data)
+            }
+        }
+
+        // function userTagShow(show) {
+        //     self.showPaint = !show
+        //     sendToH5({
+        //         type: "tagShow",
+        //         data: show,
+        //     })
+        // }
+
+        // function userTagSwitch(index) {
+        //     sendToH5({
+        //         type: "tagSwitch",
+        //         data: index,
+        //     })
+        // }
+
+        this.$once("hook:beforeDestroy", () => {
+            this.$bus.off("shop/sync/action", syncAction)
+            if (this.role == "leader") {
+                // this.$bus.off("shop/tag/onshow", userTagShow)
+                // this.$bus.off("shop/tag/onswitch", userTagSwitch)
+            }
+        })
+        this.$bus.on("shop/sync/action", syncAction)
+
+        if (this.role == "leader" && this.orgin!='fashilong') {
+            sendToApp("invite")
+            // this.$bus.on("shop/tag/onshow", userTagShow)
+            // this.$bus.on("shop/tag/onswitch", userTagSwitch)
+        }
+        
+        if (browser.urlHasValue("shopping") && this.player.progres != -1) {
+            this.$bus.on("loaded", () => {
+                startCall()
+                sendToApp('ready',this.$config.projectNum)
+            })
+        } else {
+            startCall()
+            sendToApp('ready',this.$config.projectNum)
+        }
+
+        window.CHECKEXIT = function() {
+            self.showExit = true
+        }
+    },
+    destroyed() {
+        window.CHECKEXIT = null
+    },
+    methods: {
+        onInvite() {
+            sendToApp("invite")
+        },
+        onSetMic() {
+            sendToApp("mic")
+        },
+        onExit(isExit, isNewRoom) {
+            this.showExit = false
+            this.showDissolve = false
+            this.showFull = false
+            this.showOver = false
+            if (isExit) {
+                if (this.goods) {
+                    if (this.goods.realShopUrl) {
+                        // 外部链接时隐藏购物车
+                        sendToApp("cart", false)
+                        window.location.href = this.goods.realShopUrl
+                    } else {
+                        window.parent.wx.miniProgram.navigateTo({
+                            url: "/pages/goods/goods?id=" + this.goods.id,
+                        })
+                    }
+                } else {
+                    if (isNewRoom) {
+                        sendToApp("newRoom")
+                        setTimeout(() => {
+                            this.$parent.setPage("Home")
+                        }, 300)
+                    } else {
+                        stopCall()
+                        sendToApp("exit")
+                    }
+                }
+            }
+            this.goods = null
+        },
+        onBack() {
+            this.showExit = false
+            this.showDissolve = false
+            this.showFull = false
+            this.showOver = false
+            stopCall()
+            sendToApp("back")
+        },
+        isNoTips() {
+            return !this.showExit && !this.showFull && !this.showDissolve && !this.showOver
+        },
+        exitToDetail(goods) {
+            this.goods = goods
+            this.showExit = true
+        },
+        showMessage(content, user) {
+            this.chats.push({
+                id: `${Date.now()}${this.chats.length}`,
+                avatar: user.avatar,
+                nickname: user.nickname,
+                content: content,
+            })
+
+            setTimeout(() => {
+                this.chats.shift()
+            }, 1500)
+        },
+    },
+}
+</script>
+<style lang="scss" scoped>
+ul,
+li {
+    list-style: none;
+    margin: 0;
+    padding: 0;
+}
+.navigation {
+    position: absolute;
+    left: 0.5rem;
+    right: 0.5rem;
+    bottom: 0.8rem;
+    height: 1.5rem;
+    z-index: 999;
+    .wrapper {
+        padding: 0.2rem;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 100%;
+        height: 100%;
+        border-radius: 0.15rem;
+        background-color: rgba(0, 0, 0, 0.3);
+        &.sync {
+            &::before,
+            &::after {
+                content: "";
+                top: 0;
+                left: 0;
+                position: absolute;
+                width: 100%;
+                height: 100%;
+                border: 2px solid @color;
+                border-radius: 50%;
+                opacity: 0;
+                z-index: -1;
+            }
+            &::before {
+                background: rgba(5, 200, 174, 0.6) !important;
+                animation: shadow1 1.5s ease-out;
+                animation-iteration-count: infinite;
+                box-shadow: 1px 1px 30px @color;
+            }
+            &::after {
+                background: rgba(5, 200, 174, 0.6) !important;
+                animation: shadow2 1.5s ease-out;
+                animation-iteration-count: infinite;
+                box-shadow: 1px 1px 30px @color;
+            }
+        }
+        > div {
+            flex: 1;
+            width: 100%;
+            height: 100%;
+            ul {
+                width: 100%;
+                height: 100%;
+                display: flex;
+                align-items: center;
+            }
+            li {
+                position: relative;
+                width: 1.1rem;
+                height: 100%;
+                margin-right: 0.3rem;
+                border-radius: 0.15rem;
+                overflow: hidden;
+                &.add {
+                    background: rgba(255, 255, 255, 0.7);
+                    border: 1px solid rgba(255, 255, 255, 0.2);
+                    display: flex;
+                    justify-content: center;
+                    align-items: center;
+                    i {
+                        font-size: 1rem;
+                        margin-top: 2px;
+                    }
+                }
+                img {
+                    width: 100%;
+                    height: 100%;
+                    overflow: hidden;
+                    border-radius: 0.15rem;
+                }
+                span.voice {
+                    padding: 0.01rem 0.04rem;
+                    position: absolute;
+                    right: 0.1rem;
+                    top: 0.1rem;
+                    border-radius: 0.1rem;
+                    background-color: rgba(0, 0, 0, 0.5);
+                    i {
+                        font-size: 0.3rem;
+                        &.iconscene_mic_off {
+                            color: #f56c6c;
+                        }
+                        &.speak {
+                            animation: speak 1s ease-out;
+                            animation-iteration-count: infinite;
+                        }
+                    }
+                }
+                span.status {
+                    position: absolute;
+                    width: 100%;
+                    height: 100%;
+                    background-color: rgba(0, 0, 0, 0.5);
+                    color: #fff;
+                    left: 0;
+                    top: 0;
+                    font-size: 0.2rem;
+                    display: flex;
+                    justify-content: center;
+                    align-items: center;
+                    span {
+                        white-space: nowrap;
+                    }
+                }
+                div {
+                    padding: 0.025rem;
+                    position: absolute;
+                    left: 0;
+                    bottom: -0.01rem;
+                    font-size: 0.2rem;
+                    border-bottom-left-radius: 0.15rem;
+                    white-space: nowrap;
+                    padding-left: 0.05rem;
+                    padding-right: 0.1rem;
+                }
+                &.leader {
+                    div {
+                        background: linear-gradient(
+                            to right,
+                            #ed5d18 0%,
+                            rgba(255, 255, 255, 0.5) 100%
+                        );
+                    }
+                }
+                &.customer {
+                    div {
+                        background: linear-gradient(
+                            to right,
+                            #ffcd3c 0%,
+                            rgba(255, 255, 255, 0.5) 100%
+                        );
+                    }
+                }
+            }
+        }
+        > button {
+            color: #fff;
+            width: 1.4rem;
+            height: 0.8rem;
+            border: none;
+            border-radius: 0.1rem;
+            background-color: #f56c6c;
+            font-size: 0.3rem;
+        }
+    }
+}
+.exit-tips {
+    padding: 0.8rem;
+    padding-bottom: 0;
+    h4 {
+        margin: 0;
+        font-size: 0.5rem;
+    }
+    div {
+        margin: 0.7rem 0;
+        font-size: 0.37rem;
+    }
+
+    button {
+        width: 100%;
+        margin-bottom: 0.5rem;
+        background-color: #fff;
+        font-size: 0.4rem;
+        color: #444;
+        height: 1.1rem;
+        border: none;
+        border-radius: 0.15rem;
+        &[type="submit"] {
+            color: #fff;
+            background-color: transparent;
+            border: solid 1px rgba(255, 255, 255, 0.85);
+        }
+    }
+}
+.chat {
+    pointer-events: none;
+    position: absolute;
+    left: 0;
+    right: 0;
+    bottom: 1.7rem;
+    z-index: 0;
+    li {
+        margin-top: 0.2rem;
+        > div {
+            display: inline-block;
+        }
+        div.message {
+            display: flex;
+            align-items: center;
+            justify-content: flex-start;
+            height: 0.7rem;
+            border-radius: 0.7rem;
+            background: rgba(0, 0, 0, 0.1);
+            padding-right: 0.2rem;
+            img {
+                width: 20px;
+                height: 20px;
+                overflow: hidden;
+                border-radius: 50%;
+                margin-left: 0.1rem;
+            }
+            span {
+                font-size: 14px;
+            }
+        }
+    }
+}
+
+.v-enter {
+    opacity: 0;
+    transform: translateY(20px);
+}
+.v-leave-to {
+    opacity: 0;
+    transform: translateY(-50px);
+
+    //transform: translateY(-200px);
+}
+
+.v-enter-active,
+.v-leave-active {
+    transition: all 0.6s ease;
+}
+
+.v-move {
+    transition: all 0.6s ease;
+}
+.v-leave-active {
+    position: absolute;
+}
+
+/deep/.share-x-window {
+    left: 1.2rem;
+    right: 1.2rem;
+    border-radius: 0.15rem;
+    @media (orientation: landscape) {
+        left: 50% !important;
+        right: auto !important;
+    }
+}
+@keyframes shadow1 {
+    0% {
+        transform: scale(0);
+        opacity: 1;
+    }
+
+    100% {
+        transform: scale(1);
+        opacity: 0;
+    }
+}
+@keyframes shadow2 {
+    0% {
+        transform: scale(0);
+        opacity: 1;
+    }
+
+    100% {
+        transform: scale(0.6);
+        opacity: 0;
+    }
+}
+@keyframes speak {
+    0% {
+        color: #fff;
+    }
+    30% {
+        color: #f4cba6;
+    }
+    50% {
+        color: #fbb77a;
+    }
+    100% {
+        color: #e75f04;
+    }
+}
+</style>

+ 80 - 0
src/components/RTC/chat/chat.vue

@@ -0,0 +1,80 @@
+<template>
+  <div id="chat">
+    <div id="contents">
+      <div class="chat-item" v-for="i,index in chatList" :key="index">
+        <div class="chat-msg">
+          <span :class="{my:i.userId==user_info.userId}" class="chat-name"> {{i.userName}} &nbsp;</span>
+          <span class="chat-content"> {{i.text}}</span>
+        </div>
+      </div>
+    </div>
+
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {};
+  },
+  props: {
+    user_info: {
+      type: Object,
+      default: {},
+    },
+    chatList: {
+      type: Array,
+      default: [],
+    },
+  },
+  components: {},
+  methods: {
+    autoScroll() {
+      let el = document.getElementById("chat");
+      let client_h = document.getElementById("chat").clientHeight;
+      let all = document.getElementById("contents").clientHeight;
+      el.scrollTo(0, client_h + all);
+    },
+  },
+};
+</script>
+
+<style scoped lang="scss">
+#chat {
+  // width: 7.03rem;
+  width: 50%;
+  max-height: 4.58rem;
+  overflow: auto;
+  position: fixed;
+  left: .44rem;
+  bottom: 2.83rem;
+  .chat-item {
+  }
+  .chat-msg {
+    width: auto;
+    border-radius: 0.44rem;
+    background: rgba(0, 0, 0, 0.3);
+    padding: 0.17rem 0.28rem;
+    box-sizing: border-box;
+
+    display: inline-block;
+    // margin-bottom: 0.17rem;
+    margin-bottom: 0.1rem;
+    .chat-name {
+      color: #70bbff;
+      font-size: 0.28rem;
+      &.my {
+        color: #ff9a6a;
+      }
+    }
+    .chat-content {
+      color: #fff;
+      font-size: 0.28rem;
+       word-break:break-all;
+    }
+  }
+  &::-webkit-scrollbar {
+    display: none;
+  }
+}
+</style>

+ 94 - 0
src/components/RTC/dialog/checkBrowser.vue

@@ -0,0 +1,94 @@
+<template>
+  <div id="checkBrowser">
+    <i class="iconfont iconshow_cancel" @click="closeCheckBrowser"></i>
+    <p class="title">建议使用以下最新版本的浏览器用于通话</p>
+    <div class="browser_list">
+      <div class="item" v-for="i,index in browserList" :key="index">
+        <div class="browser_icon">
+          <img :src="$config.getStaticResource(`img/apps/rtcLive/${i.icon}.png`)" alt="">
+        </div>
+        <div class="browser_name">{{i.name}}</div>
+        <!-- <div class="browser_version ">{{i.version}}</div> -->
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      browserList: [
+        {
+          icon: "chrome",
+          name: "Chrome",
+          version: "60",
+        },
+        {
+          icon: "firefox",
+          name: "Firefox",
+          version: "55",
+        },
+        {
+          icon: "edge",
+          name: "Edge",
+          version: "40",
+        },
+        {
+          icon: "safari",
+          name: "Safari",
+          version: "11",
+        },
+      ],
+    };
+  },
+  components: {},
+  methods: {
+    closeCheckBrowser() {
+      this.$parent.showCheckBrowser = false;
+    },
+  },
+};
+</script>
+
+<style scoped lang="scss">
+#checkBrowser {
+  position: fixed;
+
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  z-index: 1000;
+  background: rgba(0, 0, 0, 0.5);
+  padding: 1.11rem;
+  border-radius: 0.14rem;
+  text-align: center;
+  z-index: 10000;
+  .iconshow_cancel {
+    font-size: 0.42rem;
+    position: absolute;
+    right: 0.33rem;
+    top: 0.33rem;
+  }
+  .title {
+    font-size: 0.28rem;
+    color: #fff;
+  }
+  .browser_list {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin: 0.42rem 0 0 0;
+    .item {
+      img {
+        width: 1.39rem;
+        height: auto;
+      }
+      margin-right: 0.28rem;
+      &:last-of-type {
+        margin-right: 0;
+      }
+    }
+  }
+}
+</style>

+ 279 - 0
src/components/RTC/dialog/createdRoom.vue

@@ -0,0 +1,279 @@
+<template>
+  <div id="createdRoom" @click.stop>
+    <div class="created_dialog">
+      <div class="blurBox"></div>
+      <div class="content">
+        <div class="dialog_title" v-if="role == 'leader'">创建带看场景</div>
+        <div class="dialog_title" v-else>进入带看场景</div>
+        <div class="user_name">
+          <input
+            class="input_name"
+            maxlength="20"
+            v-model.trim="userName"
+            type="text"
+            :placeholder="role == 'leader' ? ' 请输入带看主持人名称' : '请输入您的昵称'"
+          />
+          <span class="limitNum">{{ userName.length }}/20</span>
+        </div>
+        <!-- <div v-if="role!='customer'" class="mode_btn">
+          <div @click="chooseMode(i.mode)" v-for="i,index in modeList" :key="index" :class="{ active: mode==i.mode }" class="mode">{{i.title}}</div>
+        </div> -->
+        <div class="created_btn">
+          <div class="created_cancel" @click="closeCreated">取消</div>
+          <div class="created_confirm" @click="createdConfirm">确认</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { Dialog } from "@/global_components/";
+import browser from "@/utils/browser";
+
+export default {
+  data() {
+    return {
+      role: browser.urlHashValue("role") || "leader",
+      mode: browser.urlHashValue("mode") || 2,
+      modeList: [
+        {
+          mode: 1,
+          title: "1V1",
+        },
+        {
+          mode: 2,
+          title: "多人模式",
+        },
+      ],
+      userName: "",
+      roomId: browser.urlHashValue("roomId"),
+    };
+  },
+
+  mounted() {},
+  components: {},
+  // created: {},
+  // mounted:{},
+  methods: {
+    getUrl(href,queryArr){
+
+      queryArr.forEach(item=>{
+        if (!browser.hasURLParam(item.key)) {
+          href += `&${item.key}=${item.val}`;
+        } else {
+          href = browser.replaceQueryString(href, item.key, item.val);
+        }
+      })
+      
+      return href
+    },
+
+    chooseMode(mode) {
+      this.mode = mode;
+    },
+    closeCreated() {
+      this.$emit("closeCreated");
+      !browser.hasURLParam("pose")
+    },
+    createdConfirm() {
+      if (this.userName == "") {
+        Dialog.toast({ content: '请输入您的昵称', type: 'error' })
+        return;
+      }
+      let name = encodeURIComponent(this.userName);
+      let hh = window.location.href
+
+
+      if (this.role == "customer") {
+        let tempUrl = this.getUrl(hh, [{
+          key:'mode',
+          val:this.mode
+        },{
+          key:'name',
+          val:name
+        },{
+          key:'role',
+          val:'customer'
+        },{
+          key:'roomId',
+          val:this.roomId
+        }])
+        // history.replaceState(null, null, hh + "&mode=" + this.mode + "&name=" + name + "&role=customer&roomId=" + this.roomId);
+        history.replaceState(null, null, tempUrl);
+      } else {
+
+        let tempUrl = this.getUrl(hh, [{
+          key:'mode',
+          val:this.mode
+        },{
+          key:'name',
+          val:name
+        },{
+          key:'role',
+          val:'leader'
+        }])
+
+        console.log(tempUrl);
+     
+        // history.replaceState(null, null,hh + "&mode=" + this.mode + "&name=" + name + "&role=leader");
+        history.replaceState(null, null, tempUrl);
+
+      }
+      this.$nextTick(() => {
+        this.$emit("createdConfirm");
+      });
+    },
+  },
+};
+</script>
+
+<style scoped lang="scss">
+#createdRoom {
+  width: 100vw;
+  height: 100%;
+  // background: rgba(0, 0, 0, 0.5);
+  background: transparent;
+  position: fixed;
+  left: 0;
+  top: 0;
+  z-index: 1000000;
+  // pointer-events: none;
+  .created_dialog {
+    width: 8.64rem;
+    min-height: 5rem;
+    // background: #ffffff;
+    pointer-events: auto;
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    // overflow: hidden;
+    border: 1px solid rgba(255, 255, 255, 0.1);
+    border-radius: 4px;
+
+    .blurBox {
+      position: absolute;
+      z-index: 1;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      background: rgba(0, 0, 0, 0.7);
+      filter: blur(1px);
+    }
+    .content {
+      position: relative;
+      z-index: 2;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+    }
+    .dialog_title {
+      font-size: 0.39rem;
+      width: 100%;
+      height: 1.39rem;
+      padding: 0 0.56rem;
+      box-sizing: border-box;
+      font-size: 0.39rem;
+      color: #fff;
+      line-height: 1.39rem;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      border-bottom-style: solid;
+      border-bottom-width: 1px;
+      border-bottom-color: rgba(255, 255, 255, 0.1);
+    }
+    .user_name {
+      width: 100%;
+      height: 1.11rem;
+      padding: 0 0.56rem;
+      box-sizing: border-box;
+      font-size: 0.39rem;
+      line-height: 1.11rem;
+      margin: 0.56rem 0;
+      position: relative;
+      .limitNum {
+        position: absolute;
+        right: 0.64rem;
+        top: 50%;
+        transform: translateY(-50%);
+        font-size: 0.33rem;
+        color: #b9bdbc;
+      }
+      .input_name {
+        font-size: 0.39rem;
+        width: 100%;
+        height: 100%;
+        line-height: 1.11rem;
+        padding: 0 1.066667rem 0 0.28rem;
+        box-sizing: border-box;
+        background: rgba(0, 0, 0, 0.5);
+        border-radius: 4px;
+        color: #fff;
+        border: none;
+        outline: none;
+        &::placeholder {
+          color: rgba(255, 255, 255, 0.3);
+        }
+      }
+    }
+    .mode_btn {
+      width: 100%;
+      height: 1.11rem;
+      padding: 0 0.56rem;
+      box-sizing: border-box;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      margin-bottom: 0.56rem;
+      > div.mode {
+        width: 3.61rem;
+        height: 100%;
+        border-radius: 0.65rem;
+        border: 0.03rem solid #fff;
+        color: #fff;
+        font-size: 0.39rem;
+        line-height: 1.11rem;
+        text-align: center;
+        box-sizing: border-box;
+        &.active {
+          color: #ed5d18;
+          border: 0.03rem solid #ed5d18;
+        }
+      }
+    }
+    .created_btn {
+      width: 100%;
+      height: 1.36rem;
+      border-top-style: solid;
+      border-top-width: 1px;
+      border-top-color: rgba(255, 255, 255, 0.1);
+      box-sizing: border-box;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 0.39rem;
+      > div {
+        width: 50%;
+        height: 1.36rem;
+        text-align: center;
+        line-height: 1.36rem;
+        font-size: 0.39rem;
+        box-sizing: border-box;
+        &.created_cancel {
+          color: #fff;
+          border-right-style: solid;
+          border-right-width: 1px;
+          border-right-color: rgba(255, 255, 255, 0.1);
+        }
+        &.created_confirm {
+          color: #ed5d18;
+        }
+      }
+    }
+  }
+}
+</style>

+ 138 - 0
src/components/RTC/dialog/index.vue

@@ -0,0 +1,138 @@
+<template>
+  <div id="dialog_index">
+    <div class="created_dialog">
+      <div class="blurBox"></div>
+      <div class="content">
+        <div class="dialog_title">{{ props.title }}</div>
+        <p class="dialog_desc">{{ props.desc }}</p>
+        <div class="created_btn">
+          <div class="end_cancel" @click="endLiveCancel">取消</div>
+          <div class="end_confirm" @click="endLiveConfirm">确认</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import browser from "@/utils/browser";
+import { onMounted, watch, defineProps, defineEmits, ref, nextTick } from "vue";
+
+const emit = defineEmits(["closeDialog","confirmDialog"]);
+
+const role = ref(browser.urlHashValue("role"));
+
+const props = defineProps({
+  title: {
+    type: String,
+    default: "温馨提示",
+  },
+  desc: {
+    type: String,
+    default: "是否结束带看",
+  },
+});
+
+const endLiveCancel = () => {
+  emit("closeDialog");
+};
+
+const endLiveConfirm = () => {
+  emit("confirmDialog");
+};
+</script>
+
+<style scoped lang="scss">
+#dialog_index {
+  width: 100vw;
+  height: 100%;
+  // background: rgba(0, 0, 0, 0.5);
+  position: fixed;
+  left: 0;
+  top: 0;
+  z-index: 100000;
+  pointer-events: none;
+  .created_dialog {
+    pointer-events: auto;
+    width: 8.64rem;
+    // min-height: 5rem;
+    // background: #ffffff;
+    border-radius: 8px;
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    border: 1px solid rgba(255, 255, 255, 0.1);
+    border-radius: 4px;
+    .blurBox {
+      position: absolute;
+      z-index: 1;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      background: rgba(0, 0, 0, 0.7);
+      filter: blur(1px);
+    }
+    .content {
+      position: relative;
+      z-index: 2;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+    }
+    .dialog_title {
+      width: 100%;
+      height: 1.39rem;
+      padding: 0 0.56rem;
+      box-sizing: border-box;
+      font-size: 0.39rem;
+      color: #fff;
+      line-height: 1.39rem;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      border-bottom-style: solid;
+      border-bottom-width: 1px;
+      border-bottom-color: rgba(255, 255, 255, 0.1);
+    }
+    .dialog_desc {
+      font-size: 0.42rem;
+      color: #fff;
+      padding: 0.56rem 0;
+      text-align: center;
+    }
+
+    .created_btn {
+      width: 100%;
+      height: 1.36rem;
+      border-top-style: solid;
+      border-top-width: 1px;
+      border-top-color: rgba(255, 255, 255, 0.1);
+      box-sizing: border-box;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 0.39rem;
+      > div {
+        width: 50%;
+        height: 1.36rem;
+        text-align: center;
+        line-height: 1.36rem;
+        font-size: 0.39rem;
+        box-sizing: border-box;
+        &.end_cancel {
+          color: #fff;
+          border-right-style: solid;
+          border-right-width: 1px;
+          border-right-color: rgba(255, 255, 255, 0.1);
+        }
+        &.end_confirm {
+          color: #ed5d18;
+        }
+      }
+    }
+  }
+}
+</style>

+ 169 - 0
src/components/RTC/dialog/share.vue

@@ -0,0 +1,169 @@
+<template>
+  <div id="dialog_index">
+    <div class="created_dialog">
+      <div class="blurBox"></div>
+      <div class="content">
+        <div class="dialog_title">{{ title }}</div>
+        <div class="dialog_link">
+          <p>
+            {{ shareLink }}
+          </p>
+        </div>
+
+        <div class="created_btn">
+          <div class="created_cancel" @click="closeCreated">取消</div>
+          <div class="created_confirm"  ref="copylink$" :data-clipboard-text="shareLink" @click="createdConfirm">复制分享</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { onMounted, watch, defineProps, defineEmits, ref, nextTick } from "vue";
+import ClipboardJS from 'clipboard'
+import { Dialog } from '@/global_components/'
+
+const emit = defineEmits(["closeDialog"]);
+
+const props = defineProps({
+  title: {
+    type: String,
+    default: "邀请好友",
+  },
+  shareLink: {
+    type: String,
+    default: "",
+  },
+});
+const copylink$ = ref(null)
+
+const closeCreated = () => {
+  emit("closeDialog");
+};
+
+const createdConfirm = () => {
+  emit("closeDialog");
+};
+
+onMounted(() => {
+   nextTick(()=>{
+     new ClipboardJS(copylink$.value).on('success', function (e) {
+        e.clearSelection()
+        Dialog.toast({ content: '鏈接復製成功', type: 'success' })
+    })
+   })
+})
+
+</script>
+
+<style scoped lang="scss">
+#dialog_index {
+  width: 100vw;
+  height: 100%;
+  // background: rgba(0, 0, 0, 0.5);
+  position: fixed;
+  left: 0;
+  top: 0;
+  z-index: 100000;
+  pointer-events: none;
+  .created_dialog {
+    width: 8.64rem;
+    // min-height: 5rem;
+    // background: #ffffff;
+    border-radius: 8px;
+    position: absolute;
+    left: 50%;
+    top: 50%;
+    transform: translate(-50%, -50%);
+    pointer-events: auto;
+    border: 1px solid rgba(255, 255, 255, 0.1);
+    border-radius: 4px;
+    .blurBox {
+      position: absolute;
+      z-index: 1;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      background: rgba(0, 0, 0, 0.7);
+      filter: blur(1px);
+    }
+    .content {
+      position: relative;
+      z-index: 2;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+    }
+    .dialog_title {
+      width: 100%;
+      height: 1.39rem;
+      padding: 0 0.56rem;
+      box-sizing: border-box;
+      font-size: 0.39rem;
+      color: #fff;
+      line-height: 1.39rem;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      border-bottom-style: solid;
+      border-bottom-width: 1px;
+      border-bottom-color: rgba(255, 255, 255, 0.1);
+    }
+    .dialog_link {
+      width: 100%;
+      font-size: 0.39rem;
+      color: rgba(255, 255, 255, 0.5);
+      padding: 0.53rem 0.56rem;
+      box-sizing: border-box;
+      text-align: justify;
+      text-align: left;
+      > p {
+        background: rgba(0, 0, 0, 0.5);
+        padding: 0.15rem 0.28rem;
+        word-break: break-all;
+        word-wrap: break-word;
+        text-overflow: -o-ellipsis-lastline;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        display: -webkit-box;
+        -webkit-line-clamp: 2;
+        line-clamp: 2;
+        -webkit-box-orient: vertical;
+        line-height: 0.72rem;
+      }
+    }
+
+    .created_btn {
+      width: 100%;
+      height: 1.36rem;
+      border-top-style: solid;
+      border-top-width: 1px;
+      border-top-color: rgba(255, 255, 255, 0.1);
+      box-sizing: border-box;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      > div {
+        width: 50%;
+        height: 1.36rem;
+        text-align: center;
+        line-height: 1.36rem;
+        font-size: 0.39rem;
+        box-sizing: border-box;
+        &.created_cancel {
+          color: #fff;
+          border-right-style: solid;
+          border-right-width: 1px;
+          border-right-color: rgba(0, 0, 0, 0.05);
+        }
+        &.created_confirm {
+          color: #ed5d18;
+        }
+      }
+    }
+  }
+}
+</style>

+ 73 - 0
src/components/RTC/index.vue

@@ -0,0 +1,73 @@
+<template>
+  <transition mode="out-in">
+    <div>
+      <dialogIndex @closeDialog="closeDialog" @confirmDialog="confirmDialog" v-if="dialog == 'dialogIndex'"></dialogIndex>
+      <dialogShare :shareLink="shareLink" @closeDialog="closeDialog" v-if="dialog == 'dialogShare'"></dialogShare>
+      <createdRoom v-if="showCreated" @closeCreated="closeCreated" @createdConfirm="createdConfirm()"></createdRoom>
+      <PageRtcLive @openDialog="openDialog" v-if="show"></PageRtcLive>
+    </div>
+  </transition>
+</template>
+
+<script setup>
+import PageRtcLive from "./PageRtcLive";
+// import Draw from "./paint/Draw";
+import createdRoom from "./dialog/createdRoom";
+import dialogIndex from "./dialog/index.vue";
+import dialogShare from "./dialog/share.vue";
+import browser from "@/utils/browser";
+import { onMounted, watch, computed, ref, nextTick } from "vue";
+import { useStore } from "vuex";
+
+const store = useStore();
+
+const shareLink = ref("");
+const dialog = ref("");
+const show = ref(false);
+const showPaint = ref(true);
+const showCreated = ref(false);
+const roomId = ref(browser.getURLParam("roomId"));
+const role = ref(browser.getURLParam("role"));
+const userName = ref(browser.getURLParam("name"));
+
+const openDialog = (str, link) => {
+  shareLink.value = link;
+  dialog.value = str;
+};
+const closeDialog = (str, link) => {
+  dialog.value = "";
+  dialog.value = str;
+};
+
+const confirmDialog = () => {
+  store.commit("showShoppingguide", false);
+  let tempUrl = window.location.href;
+  ["mode", "name", "role", "roomId"].forEach((item) => {
+    tempUrl = browser.replaceQueryString(tempUrl, item, "");
+  });
+  history.replaceState(null, null, tempUrl);
+
+  store.commit("showShoppingguide", false);
+  dialog.value = "";
+};
+
+const closeCreated = (str, link) => {
+  store.commit("showShoppingguide", false);
+  showCreated.value = false;
+};
+
+const createdConfirm = (str, link) => {
+  showCreated.value = false;
+  show.value = true;
+};
+
+if (roomId.value && !userName.value) {
+  showCreated.value = true;
+}
+
+onMounted(() => {
+  showCreated.value = true;
+});
+</script>
+
+<style lang="scss" scoped></style>

+ 481 - 0
src/components/RTC/paint/Draw.vue

@@ -0,0 +1,481 @@
+<template>
+  <div class="paint">
+    <canvas ref="canvas" v-show="show"></canvas>
+    <!-- <div class="toolbar" v-show="showPaint">
+      <ul>
+        <li @click="onDraw('drawStart')" v-show="show == false">
+          <i class="iconfont icontagging"></i>
+          <div>画笔</div>
+        </li>
+        <li @click="onDraw('drawUndo')" v-if="show" :class="{ disable: !canUndo }">
+          <i class="iconfont icon_cancel"></i>
+          <div>撤回</div>
+        </li>
+        <li @click="onDraw('drawStop')" v-if="show">
+          <i class="iconfont iconclose"></i>
+          <div>关闭</div>
+        </li>
+      </ul>
+    </div> -->
+  </div>
+</template>
+<script>
+import { getRole, sendToH5 } from "../../../rtc_socket";
+import { objects } from "@/core/base";
+import math from "@/core/util/math";
+import convertTool from "@/core/util/convertTool";
+
+export default {
+  props: {
+    showPaint: Boolean,
+  },
+  data() {
+    return {
+      role: getRole(),
+      show: false,
+      canUndo: false,
+      colorA: "#02c8ae",
+      colorB: "#2e98fe",
+    };
+  },
+  watch: {
+    showPaint() {
+      if (this.showPaint) {
+        this.role = getRole();
+      }
+    },
+    show() {
+      this.$bus.emit("shop/header/disable", this.show);
+    },
+  },
+  created() {
+    this.$bus.on("shop/sync/action", (data) => {
+      if (data.type == "drawStart") {
+        this.show = true;
+        this.draw = [];
+        this.drawHistory = [];
+        this.$nextTick(() => {
+          this.onDrawStart();
+        });
+      } else if (data.type == "drawStop") {
+        this.show = false;
+        this.draw = null;
+        this.drawHistory = null;
+      } else if (data.type == "drawing") {
+        const draw = this.transformTo2d(data.data.drawing);
+        if (data.data.role != this.role) {
+          this.drawHistory.push(JSON.parse(JSON.stringify(draw)));
+          this.drawing(draw);
+        }
+      } else if (data.type == "drawUndo") {
+        this.drawUndo(data.data.role);
+      }
+    });
+  },
+  mounted() {
+    this.canvas = this.$refs.canvas;
+    this.context = this.canvas.getContext("2d");
+    this.canvas.onmousedown = (e) => {
+      if (!this.show || this.role != "leader") {
+        return;
+      }
+      e.preventDefault();
+      this.beginStroke({
+        x: e.clientX,
+        y: e.clientY,
+      });
+    };
+    this.canvas.onmouseup = (e) => {
+      if (!this.show || this.role != "leader") {
+        return;
+      }
+      e.preventDefault();
+      this.endStroke();
+    };
+    this.canvas.onmouseout = (e) => {
+      if (!this.show || this.role != "leader") {
+        return;
+      }
+      e.preventDefault();
+      this.endStroke();
+    };
+    this.canvas.onmousemove = (e) => {
+      if (!this.show || this.role != "leader") {
+        return;
+      }
+      e.preventDefault();
+      if (this._mouseDown) {
+        this.moveStroke({
+          x: e.clientX,
+          y: e.clientY,
+        });
+      }
+    };
+
+    let touch;
+
+    // 移动端触控
+    this.canvas.addEventListener("touchstart", (e) => {
+      if (!this.show || this.role != "leader") {
+        return;
+      }
+      e.preventDefault();
+      touch = e.touches[0];
+      this.beginStroke({
+        x: touch.pageX,
+        y: touch.pageY,
+      });
+    });
+    this.canvas.addEventListener("touchmove", (e) => {
+      if (!this.show || this.role != "leader") {
+        return;
+      }
+      e.preventDefault();
+      if (this._mouseDown) {
+        touch = e.touches[0];
+        this.moveStroke({
+          x: touch.pageX,
+          y: touch.pageY,
+        });
+      }
+    });
+
+    this.canvas.addEventListener("touchend", (e) => {
+      if (!this.show || this.role != "leader") {
+        return;
+      }
+      e.preventDefault();
+      this.endStroke();
+    });
+
+    this.mouse = new THREE.Vector2();
+  },
+  methods: {
+    /**
+     * 2d数据转3d数据
+     */
+    transformTo3d(draw) {
+      const data = [];
+      if (draw.length == 0) {
+        return [];
+      }
+      draw.forEach((item, index) => {
+        math.convertScreenPositionToNDC(item.x, item.y, this.mouse);
+
+        var intersect = convertTool.getMouseIntersect(
+          objects.player.camera,
+          [this.intersectPlane],
+          this.mouse
+        );
+
+        if (!intersect) {
+          console.error("no intersect ??");
+        } else {
+          item.pos3d = intersect.point;
+          data.push(item);
+        }
+      });
+
+      return data;
+    },
+    /**
+     * 3d数据转2d数据
+     */
+    transformTo2d(draw) {
+      const data = [];
+      draw.forEach((item) => {
+        var pos3d = new THREE.Vector3(item.pos3d.x, item.pos3d.y, item.pos3d.z);
+        var pos2d = convertTool.getPos2d(pos3d, objects.player.camera);
+        //delete item.pos3d;
+        item.x = pos2d.pos.x;
+        item.y = pos2d.pos.y;
+        data.push(item);
+      });
+      return data;
+    },
+    onDraw(type) {
+      if (type == "drawStart") {
+        this.show = true;
+        this.draw = [];
+        this.drawHistory = [];
+        this.$nextTick(() => {
+          this.onDrawStart();
+        });
+      } else if (type == "drawStop") {
+        this.show = false;
+        this.draw = null;
+        this.drawHistory = null;
+      } else if (type == "drawing") {
+        const draw = this.transformTo2d(data.content.drawing);
+        if (data.role != role) {
+          this.drawHistory.push(JSON.parse(JSON.stringify(draw)));
+          //this.drawing(draw);
+        }
+      } else if (type == "drawUndo") {
+        this.drawUndo(this.role);
+      }
+      sendToH5({
+        type,
+        data: {
+          role: this.role,
+        },
+      });
+    },
+    onPainting() {
+      const draw = this.transformTo3d(this.draw);
+      this.drawHistory.push(JSON.parse(JSON.stringify(draw)));
+      sendToH5({
+        type: "drawing",
+        data: {
+          drawing: draw,
+        },
+      });
+      this.draw = [];
+      this.canUndo = true;
+      this._endTime = 0;
+      this._mouseDown = false;
+      this._lastTimestamp = 0;
+      this.$emit("sendCanUndo", this.canUndo);
+    },
+    beginStroke(point) {
+      this._mouseDown = true;
+      this._lastTimestamp = Date.now();
+      this._lastPosition = this.windowToCanvas(point.x, point.y);
+      this.draw.push({
+        role: this.role,
+        width: 0,
+        x: this._lastPosition.x,
+        y: this._lastPosition.y,
+        t: 5, //this._lastTimestamp - this._endTime
+      });
+    },
+    onDrawStart() {
+      let dpr = window.devicePixelRatio || 1;
+      let rect = this.canvas.getBoundingClientRect();
+      this.ratio = 1; // window.innerWidth / 375;
+      this.canvas.width = rect.width * dpr;
+      this.canvas.height = rect.height * dpr;
+      this.context.scale(dpr, dpr);
+      this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
+      this._endTime = 0;
+      this._mouseDown = false;
+      this._lastTimestamp = 0;
+      this._lastLineWidth = -1;
+      this._lastPosition = {
+        x: 0,
+        y: 0,
+      };
+
+      math.convertScreenPositionToNDC(0, 0, this.mouse); //取屏幕中点
+      var intersect = convertTool.getMouseIntersect(
+        objects.player.camera,
+        [objects.model.skybox, objects.sceneRenderer.scene.skyboxBG],
+        this.mouse
+      );
+      this.placeIntersectPlane(intersect && intersect.point);
+    },
+    moveStroke(point) {
+      let timestamp = Date.now();
+      let position = this.windowToCanvas(point.x, point.y);
+      let s = this.calcDistance(position, this._lastPosition);
+      let t = timestamp - this._lastTimestamp;
+      let lineWidth = this.calcLineWidth(t, s);
+
+      //draw
+      this.context.beginPath();
+      this.context.moveTo(this._lastPosition.x, this._lastPosition.y);
+      this.context.lineTo(position.x, position.y);
+
+      this.draw.push({
+        role: this.role,
+        width: lineWidth,
+        x: position.x,
+        y: position.y,
+        t: 5, //t
+      });
+
+      this.context.strokeStyle = this.colorA;
+      this.context.lineWidth = lineWidth;
+      this.context.lineCap = "round";
+      this.context.linJoin = "round";
+      this.context.stroke();
+
+      //每次过程结束时,将结束值赋给初始值,一直延续
+      this._lastPosition = position;
+      this._lastTimestamp = timestamp;
+      this._lastLineWidth = lineWidth;
+    },
+    endStroke() {
+      this.draw.push({
+        role: this.role,
+        width: 0,
+        x: this._lastPosition.x,
+        y: this._lastPosition.y,
+        t: 0,
+      });
+      this.onPainting();
+      this._mouseDown = false;
+      this._endTime = Date.now();
+    },
+    calcLineWidth(t, s) {
+      let v = s / t;
+      let resultLineWidth;
+
+      if (v <= 0.1) {
+        resultLineWidth = 6;
+      } else if (v >= 3) {
+        resultLineWidth = 2;
+      } else {
+        resultLineWidth = 6 - ((v - 0.1) / (3 - 0.1)) * (6 - 4);
+      }
+      if (this._lastLineWidth == -1) {
+        return resultLineWidth;
+      }
+      return (this._lastLineWidth * 2) / 3 + (resultLineWidth * 1) / 3;
+    },
+    calcDistance(pos1, pos2) {
+      return Math.sqrt(
+        (pos1.x - pos2.x) * (pos1.x - pos2.x) +
+          (pos1.y - pos2.y) * (pos1.y - pos2.y)
+      ); //通过起始结束坐标x,y值计算路程长度
+    },
+    windowToCanvas(x, y) {
+      var bbox = this.canvas.getBoundingClientRect(); //获取canvas的位置信息
+      return {
+        x: Math.round(x - bbox.left),
+        y: Math.round(y - bbox.top),
+      }; //返回当前鼠标相对于canvas的位置
+    },
+    drawing(draw) {
+      for (let i = 0; i < draw.length - 1; i++) {
+        draw[i].t &&
+          setTimeout(() => {
+            this.context.beginPath();
+            this.context.strokeStyle =
+              draw[i].role == this.role ? this.colorA : this.colorB;
+            this.context.moveTo(draw[i].x * this.ratio, draw[i].y * this.ratio);
+            this.context.lineTo(
+              draw[i + 1].x * this.ratio,
+              draw[i + 1].y * this.ratio
+            );
+            this.context.lineWidth = draw[i].width * this.ratio;
+            this.context.lineCap = "round";
+            this.context.linJoin = "round";
+            this.context.stroke();
+          }, 5);
+      }
+    },
+    drawUndo(sender) {
+      this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
+      for (let i = this.drawHistory.length - 1; i >= 0; i--) {
+        if (this.drawHistory[i][0].role == sender) {
+          this.drawHistory.splice(i, 1);
+          break;
+        }
+      }
+      this.drawHistory.forEach((draw) => {
+        for (let i = 0; i < draw.length - 1; i++) {
+          if (draw[i].t) {
+            this.context.beginPath();
+            this.context.strokeStyle =
+              draw[i].role == this.role ? this.colorA : this.colorB;
+            this.context.moveTo(draw[i].x * this.ratio, draw[i].y * this.ratio);
+            this.context.lineTo(
+              draw[i + 1].x * this.ratio,
+              draw[i + 1].y * this.ratio
+            );
+            this.context.lineWidth = draw[i].width * this.ratio;
+            this.context.lineCap = "round";
+            this.context.linJoin = "round";
+            this.context.stroke();
+          }
+        }
+      });
+      this.canUndo = this.drawHistory.some((item) => item[0].role == this.role);
+    },
+    placeIntersectPlane(pos) {
+      //用于判断mesh拖拽移动距离的平面 需要和视线垂直,以保证遮住视野范围
+      if (!this.intersectPlane) {
+        var geo = new THREE.PlaneGeometry(8000, 80000, 1, 1);
+        //var geo = new THREE.PlaneGeometry(3,3,1,1);
+        this.intersectPlane = new THREE.Mesh(
+          geo,
+          new THREE.MeshBasicMaterial({
+            transparent: true,
+            wireframe: false,
+            opacity: 0,
+            side: THREE.DoubleSide,
+            depthTest: false,
+          })
+        );
+        this.intersectPlane.lookAt(new THREE.Vector3(0, 1, 0));
+        this.intersectPlane.name = "intersectPlane";
+        objects.model.add(this.intersectPlane);
+      }
+      if (pos) {
+        this.intersectPlane.position.copy(pos);
+        var cameraDir = objects.player.getDirection(
+          null,
+          objects.player.camera
+        ); //向里
+        this.intersectPlane.lookAt(pos.clone().add(cameraDir)); //看向相机
+      }
+    },
+  },
+};
+</script>
+<style lang="scss" scoped>
+.paint {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 2000;
+  pointer-events: none !important;
+  canvas {
+    width: 100vw;
+    height: 100vh;
+    image-rendering: pixelated;
+    image-rendering: crisp-edges;
+    pointer-events: auto;
+    position: fixed;
+    top: 0;
+    left: 0;
+  }
+  .toolbar {
+    pointer-events: auto;
+    position: absolute;
+    right: 0.35rem;
+    bottom: 4.5rem;
+    padding: 0.4rem 0.2rem;
+    border-radius: 30px;
+    z-index: 100;
+    background-color: rgba(0, 0, 0, 0.3);
+    ul,
+    li {
+      margin: 0;
+      padding: 0;
+      list-style: none;
+    }
+    li {
+      padding: 0.3px;
+      text-align: center;
+      font-size: 14px;
+      margin-bottom: 0.5rem;
+      &:last-child {
+        margin-bottom: 0;
+      }
+      i {
+        font-size: 20px;
+        &.icon_cancel {
+          font-size: 22px;
+        }
+        &.iconclose {
+          font-size: 14px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 308 - 0
src/components/RTC/paint/index.vue

@@ -0,0 +1,308 @@
+<template>
+    <div class="paint" v-show="showPaint && role == 'leader'">
+        <div class="toolbar">
+            <ul>
+                <li v-show="show == false" @click="start">
+                    <i class="iconfont iconkd_tagging"></i>
+                    <div>标记</div>
+                </li>
+                <li v-if="show" @click="end">
+                    <i class="iconfont iconclose"></i>
+                    <div>关闭</div>
+                </li>
+            </ul>
+        </div>
+    </div>
+</template>
+<script>
+import { app } from "@/core"
+import { objects } from "@/core/base"
+import config from "@/config"
+import { getRole, sendToH5,sendToApp } from "../../../socket"
+import settings from "@/core/settings"
+import math from "@/core/util/math"
+import convertTool from "@/core/util/convertTool"
+import texture from "@/core/util/texture.js"
+import lerp from "@/core/util/lerp"
+import transitions from "@/core/util/transitions"
+import easing from "@/core/util/easing"
+
+let meshGroup = null //new THREE.Object3D()
+let circles = []
+const map = texture.load(config.getStaticResource("img/scene_tabele.png?v=1"), null, null, {
+    antialias: false,
+})
+
+export default {
+    props: {
+        showPaint: Boolean,
+    },
+    data() {
+        return {
+            role: getRole(),
+            show: false,
+            flags: [],
+            canUndo: false,
+            colorA: "#02c8ae",
+            colorB: "#2e98fe",
+        }
+    },
+    created() {
+        const self = this
+
+        if (meshGroup == null) {
+            meshGroup = new THREE.Object3D()
+            meshGroup.name = "shop-circles"
+            if (app.ready) {
+                app.model.add(meshGroup)
+            } else {
+                this.$bus.on("ready", function() {
+                    app.model.add(meshGroup)
+                })
+            }
+        }
+
+        function remove() {
+            setTimeout(() => {
+                self.flags.shift()
+                if (self.flags.length) {
+                    remove()
+                }
+                self.updateCirclesDrawing(self.flags)
+            }, 1000)
+        }
+
+        function syncAction(data) {
+            if (data.type == "flags") {
+                self.flags.push(data.data)
+                self.updateCirclesDrawing(self.flags)
+                remove()
+            }
+        }
+
+        this.$bus.on("shop/sync/action", syncAction)
+        this.$once("hook:beforeDestroy", () => {
+            this.$bus.off("shop/sync/action", syncAction)
+            this.show = false
+            this.flags = []
+            this.updateCirclesDrawing(this.flags)
+        })
+    },
+    watch: {
+        show() {
+            sendToApp('cart',!this.show)
+            this.$bus.emit("shop/header/disable", this.show)
+            setTimeout(() => {
+                app.play.control.noFly = this.show
+            }, 300);
+        },
+    },
+    mounted() {
+        var self = this
+        function draw(e) {
+            if (!self.show) {
+                return
+            }
+            const x = (e.touches[0] || e).clientX
+            const y = (e.touches[0] || e).clientY
+            const f = {
+                sid: Date.now(),
+                pos: self.transformTo3d({ x, y }),
+                color: self.colorA,
+            }
+            self.flags.push(f)
+            self.updateCirclesDrawing(self.flags)
+
+             setTimeout(() => {
+                self.flags.shift()
+                self.updateCirclesDrawing(self.flags)
+            }, 1000)
+
+            sendToH5({ type: "flags", data: f })
+           
+        }
+        const $player = document.getElementById("player")
+        $player.addEventListener("mousedown", draw)
+        $player.addEventListener("touchstart", draw)
+        this.role = getRole()
+    },
+    methods: {
+        start() {
+            this.show = true
+        },
+        undo() {
+            this.flags.pop()
+            this.updateCirclesDrawing(this.flags)
+        },
+        end() {
+            this.show = false
+            this.flags = []
+            this.updateCirclesDrawing(this.flags)
+        },
+        updateCirclesDrawing(flags) {
+            //呈现一系列circles  其中包含已经绘制好的
+
+            var dels = []
+            circles.forEach(circle => {
+                //检查是否有要删除的
+                var find = flags.find(info => circle.sid == info.sid)
+                if (!find) {
+                    dels.push(circle)
+                }
+            })
+            dels.forEach(circle => this.fadeCircle(circle))
+
+            flags.forEach(info => {
+                //再添加新的
+                var circle = meshGroup.children.find(circle => circle.sid == info.sid)
+                if (circle) {
+                    //如果要修改什么属性在这修改
+                } else {
+                    this.createCircle(info.sid, info.pos, info.color)
+                }
+            })
+        },
+
+        transformTo3d(pos2d) {
+            //获取三维坐标
+            const data = new THREE.Vector3()
+            const mouse = new THREE.Vector2()
+
+            math.convertScreenPositionToNDC(pos2d.x, pos2d.y, mouse)
+
+            var intersect = convertTool.getMouseIntersect(
+                objects.player.camera,
+                [objects.model.skybox],
+                mouse
+            )
+
+            if (!intersect) {
+                console.error("no intersect ??")
+            } else {
+                data.copy(intersect.point)
+                var dir = data
+                    .clone()
+                    .sub(objects.player.position)
+                    .normalize()
+                data.copy(objects.player.position.clone().add(dir))
+            }
+
+            return data
+        },
+        createCircle(sid, pos, color) {
+            //绘制新的圈
+
+            var circle = new THREE.Sprite(this.getMat(color))
+            circle.sid = sid
+            circle.position.copy(pos)
+            circle.quaternion.copy(objects.player.camera.quaternion)
+            circle.scale.set(0.05, 0.05, 0.05)
+            meshGroup.add(circle)
+            circles.push(circle)
+            /*var scale = math.getScaleForConstantSize({
+                width2d:100,
+                camera:objects.player.camera,
+                position: pos
+            })
+            bubble.scale.copy(scale)*/
+        },
+
+        fadeCircle(circle) {
+            //开始消失
+            //console.log("fadeCircle"+circle.sid)
+            var _duration = 800
+            transitions.start(
+                lerp.property(circle.material, "opacity", 0),
+                _duration,
+                () => {
+                    this.removeCircle(circle)
+                },
+                0,
+                easing[settings.transition.blendEasing],
+                "shop-circles-fade",
+                settings.freeze.shopCircle
+            )
+
+            //从列表中删除
+            var index = circles.indexOf(circle)
+            circles.splice(index, 1)
+        },
+
+        removeCircle(circle) {
+            //移除某个圈 <!-- sid -->
+            //var circle = meshGroup.children.find(circle=>circle.sid == sid);
+            //console.log("removeC " + circle.sid)
+            circle.material.dispose()
+            circle.parent.remove(circle)
+        },
+
+        getMat(color) {
+            /*var mat = this.materials[color]
+            if (mat) return mat
+            else {
+                mat = new THREE.SpriteMaterial({
+                    transparent: true,
+                    depthTest: false,
+                    map: map,
+                    side: THREE.DoubleSide,
+                    color: color,
+                })
+                this.materials[color] = mat
+                return mat
+            }*/
+            return new THREE.SpriteMaterial({
+                transparent: true,
+                depthTest: false,
+                map: map,
+                side: THREE.DoubleSide,
+                color: color,
+            })
+        },
+    },
+}
+</script>
+<style lang="scss" scoped>
+.paint {
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    z-index: 99;
+    pointer-events: none !important;
+    .toolbar {
+        pointer-events: auto;
+        position: absolute;
+        right: 0.35rem;
+        bottom: 4.5rem;
+        padding: 0.4rem 0.2rem;
+        border-radius: 30px;
+        z-index: 100;
+        background-color: rgba(0, 0, 0, 0.3);
+        ul,
+        li {
+            margin: 0;
+            padding: 0;
+            list-style: none;
+        }
+        li {
+            padding: 0.3px;
+            text-align: center;
+            font-size: 14px;
+            margin-bottom: 0.5rem;
+            &:last-child {
+                margin-bottom: 0;
+            }
+            i {
+                font-size: 20px;
+                &.icon_cancel {
+                    font-size: 22px;
+                }
+                // &.iconclose {
+                //     font-size: 14px;
+                // }
+            }
+        }
+    }
+}
+</style>

+ 9 - 0
src/components/RTC/socket/index.js

@@ -0,0 +1,9 @@
+import browser from "@/utils/browser"
+
+
+export let userName = decodeURIComponent(browser.urlHashValue("name"))
+export let roomId = browser.urlHashValue("roomId") || uuid(24)
+export let userId = browser.urlHashValue("userId") || uuid(12)
+export let mode = browser.urlHashValue("mode") //
+export let role = browser.urlHashValue("role") || "leader" //customer 普通用户
+export let muted = false //默认不静音

+ 5 - 0
src/store/index.js

@@ -20,6 +20,7 @@ const store = createStore({
             },
             router: {},
             controlsBottom: '20px',
+            shoppingguide: false
         }
     },
     getters: {
@@ -29,8 +30,12 @@ const store = createStore({
         flying: state => state.flying,
         floorId: state => state.floorId || 0,
         controlsBottom: state => state.controlsBottom,
+        shoppingguide: state => state.shoppingguide
     },
     mutations: {
+        showShoppingguide(state, payload) {
+            state.shoppingguide = payload
+        },
         /**
          * 设置相机模式状态
          * @param {*} state

+ 3 - 1
src/store/modules/scene.js

@@ -7,7 +7,7 @@ export default {
         return {
             tags: [],
             floors: [],
-            metadata: {},
+            metadata: {}
         }
     },
     getters: {
@@ -20,6 +20,7 @@ export default {
         metadata: state => {
             return state.metadata
         },
+        
         musicURL: (state, getters, rootState, rootGetters) => {
             let metadata = getters.metadata
             if (metadata.music) {
@@ -39,6 +40,7 @@ export default {
         },
     },
     mutations: {
+       
         load(state, payload) {
             state.metadata = payload
             document.title = payload.title

+ 19 - 15
src/utils/browser.js

@@ -37,10 +37,10 @@ var browser = {
     dom.requestFullscreen
       ? dom.requestFullscreen()
       : dom.mozRequestFullScreen
-      ? dom.mozRequestFullScreen()
-      : dom.webkitRequestFullscreen
-      ? dom.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT)
-      : dom.msRequestFullscreen && dom.msRequestFullscreen(),
+        ? dom.mozRequestFullScreen()
+        : dom.webkitRequestFullscreen
+          ? dom.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT)
+          : dom.msRequestFullscreen && dom.msRequestFullscreen(),
       t && $(document).on("fullscreenchange webkitfullscreenchange mozfullscreenchange MSFullscreenChange", browser.requestPointerLock);
   },
   requestPointerLock: function () {
@@ -63,19 +63,19 @@ var browser = {
     document.exitFullscreen
       ? document.exitFullscreen()
       : document.msExitFullscreen
-      ? document.msExitFullscreen()
-      : document.mozCancelFullScreen
-      ? document.mozCancelFullScreen()
-      : document.webkitExitFullscreen && document.webkitExitFullscreen();
+        ? document.msExitFullscreen()
+        : document.mozCancelFullScreen
+          ? document.mozCancelFullScreen()
+          : document.webkitExitFullscreen && document.webkitExitFullscreen();
   },
   details: function () {
     var e = navigator.userAgent.match("(Firefox|Chrome|Safari)/([\\d]+)");
     return e
       ? {
-          name: e[1],
-          version: parseInt(e[2]),
-          platform: navigator.platform,
-        }
+        name: e[1],
+        version: parseInt(e[2]),
+        platform: navigator.platform,
+      }
       : {};
   },
   is: function (e) {
@@ -178,8 +178,8 @@ var browser = {
     var e = $.Deferred();
     return this.detectWebVR()
       ? (navigator.getVRDisplays().then(function (t) {
-          t.length >= 1 && e.resolve(t[0]), e.reject(null);
-        }),
+        t.length >= 1 && e.resolve(t[0]), e.reject(null);
+      }),
         e)
       : e.reject(null);
   },
@@ -354,7 +354,11 @@ var browser = {
   },
 
   replaceQueryString(url, name, value) {
-    const re = new RegExp(name + "=[^&]*", "gi");
+    let re = new RegExp(name + "=[^&]*", "gi");
+    if (!value) {
+      let ttt = url.replace(new RegExp('&' + name + "=[^&]*", "gi"), '')
+      return ttt.replace(re, '');
+    }
     return url.replace(re, name + "=" + value);
   },
   openLink(mglink, h5link, appLink) {

+ 1 - 0
src/utils/rtc_socket.js

@@ -0,0 +1 @@
+

+ 3 - 0
src/views/Panel.vue

@@ -38,6 +38,9 @@ const resetPanel = () => {
   if (panelOrgTop.value > 0) {
     KanKan.Animate.transitions.start((progress) => {
       const $panel = document.querySelector(".panel");
+      if (!$panel) {
+        return
+      }
       let orgTop = $panel.offsetTop;
       let newTop = panelOrgTop.value;
       let value = orgTop - newTop;