gemercheung пре 2 година
родитељ
комит
7cd060be4e

+ 1 - 1
.env

@@ -9,4 +9,4 @@ VITE_APP_SOCKET_URL=wss://ws.4dkankan.com
 # 静态资源目录
 VITE_APP_STATIC_DIR=viewer
 # 接口请求地址
-VITE_APP_APIS_URL=https://test.4dkankan.com/
+VITE_APP_APIS_URL=https://v4-uat.4dkankan.com/

+ 488 - 1
package-lock.json

@@ -8,13 +8,18 @@
       "name": "socket-international",
       "version": "0.0.0",
       "dependencies": {
+        "clipboard": "^2.0.11",
         "consola": "^2.15.3",
         "pinia": "^2.0.23",
-        "vue": "^3.2.41"
+        "uuid": "^9.0.0",
+        "vue": "^3.2.41",
+        "vue-types": "^4.2.1"
       },
       "devDependencies": {
         "@types/node": "^18.11.7",
+        "@types/uuid": "^8.3.4",
         "@vitejs/plugin-vue": "^3.2.0",
+        "sass": "^1.55.0",
         "typescript": "^4.8.4",
         "vite": "^3.2.0",
         "vite-plugin-html-env": "^1.2.7",
@@ -74,6 +79,13 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/uuid": {
+      "version": "8.3.4",
+      "resolved": "http://192.168.0.47:4873/@types/uuid/-/uuid-8.3.4.tgz",
+      "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@vitejs/plugin-vue": {
       "version": "3.2.0",
       "resolved": "http://192.168.0.47:4873/@vitejs/plugin-vue/-/plugin-vue-3.2.0.tgz",
@@ -266,6 +278,20 @@
       "integrity": "sha512-W9mfWLHmJhkfAmV+7gDjcHeAWALQtgGT3JErxULl0oz6R6+3ug91I7IErs93eCFhPCZPHBs4QJS7YWEV7A3sxw==",
       "license": "MIT"
     },
+    "node_modules/anymatch": {
+      "version": "3.1.2",
+      "resolved": "http://192.168.0.47:4873/anymatch/-/anymatch-3.1.2.tgz",
+      "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "http://192.168.0.47:4873/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -273,6 +299,16 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/binary-extensions": {
+      "version": "2.2.0",
+      "resolved": "http://192.168.0.47:4873/binary-extensions/-/binary-extensions-2.2.0.tgz",
+      "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/brace-expansion": {
       "version": "2.0.1",
       "resolved": "http://192.168.0.47:4873/brace-expansion/-/brace-expansion-2.0.1.tgz",
@@ -283,6 +319,58 @@
         "balanced-match": "^1.0.0"
       }
     },
+    "node_modules/braces": {
+      "version": "3.0.2",
+      "resolved": "http://192.168.0.47:4873/braces/-/braces-3.0.2.tgz",
+      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/chokidar": {
+      "version": "3.5.3",
+      "resolved": "http://192.168.0.47:4873/chokidar/-/chokidar-3.5.3.tgz",
+      "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://paulmillr.com/funding/"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/clipboard": {
+      "version": "2.0.11",
+      "resolved": "http://192.168.0.47:4873/clipboard/-/clipboard-2.0.11.tgz",
+      "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
+      "license": "MIT",
+      "dependencies": {
+        "good-listener": "^1.2.2",
+        "select": "^1.1.2",
+        "tiny-emitter": "^2.0.0"
+      }
+    },
     "node_modules/consola": {
       "version": "2.15.3",
       "resolved": "http://192.168.0.47:4873/consola/-/consola-2.15.3.tgz",
@@ -302,6 +390,12 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/delegate": {
+      "version": "3.2.0",
+      "resolved": "http://192.168.0.47:4873/delegate/-/delegate-3.2.0.tgz",
+      "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==",
+      "license": "MIT"
+    },
     "node_modules/esbuild": {
       "version": "0.15.12",
       "resolved": "http://192.168.0.47:4873/esbuild/-/esbuild-0.15.12.tgz",
@@ -686,6 +780,19 @@
       "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
       "license": "MIT"
     },
+    "node_modules/fill-range": {
+      "version": "7.0.1",
+      "resolved": "http://192.168.0.47:4873/fill-range/-/fill-range-7.0.1.tgz",
+      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/fsevents": {
       "version": "2.3.2",
       "resolved": "http://192.168.0.47:4873/fsevents/-/fsevents-2.3.2.tgz",
@@ -708,6 +815,28 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "http://192.168.0.47:4873/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/good-listener": {
+      "version": "1.2.2",
+      "resolved": "http://192.168.0.47:4873/good-listener/-/good-listener-1.2.2.tgz",
+      "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
+      "license": "MIT",
+      "dependencies": {
+        "delegate": "^3.1.2"
+      }
+    },
     "node_modules/has": {
       "version": "1.0.3",
       "resolved": "http://192.168.0.47:4873/has/-/has-1.0.3.tgz",
@@ -731,6 +860,26 @@
         "he": "bin/he"
       }
     },
+    "node_modules/immutable": {
+      "version": "4.1.0",
+      "resolved": "http://192.168.0.47:4873/immutable/-/immutable-4.1.0.tgz",
+      "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "http://192.168.0.47:4873/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/is-core-module": {
       "version": "2.11.0",
       "resolved": "http://192.168.0.47:4873/is-core-module/-/is-core-module-2.11.0.tgz",
@@ -744,6 +893,48 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "http://192.168.0.47:4873/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "http://192.168.0.47:4873/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "http://192.168.0.47:4873/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-plain-object": {
+      "version": "5.0.0",
+      "resolved": "http://192.168.0.47:4873/is-plain-object/-/is-plain-object-5.0.0.tgz",
+      "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/magic-string": {
       "version": "0.25.9",
       "resolved": "http://192.168.0.47:4873/magic-string/-/magic-string-0.25.9.tgz",
@@ -785,6 +976,16 @@
         "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
       }
     },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "http://192.168.0.47:4873/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/path-parse": {
       "version": "1.0.7",
       "resolved": "http://192.168.0.47:4873/path-parse/-/path-parse-1.0.7.tgz",
@@ -798,6 +999,19 @@
       "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
       "license": "ISC"
     },
+    "node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "http://192.168.0.47:4873/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
     "node_modules/pinia": {
       "version": "2.0.23",
       "resolved": "http://192.168.0.47:4873/pinia/-/pinia-2.0.23.tgz",
@@ -874,6 +1088,19 @@
         "node": "^10 || ^12 || >=14"
       }
     },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "http://192.168.0.47:4873/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
     "node_modules/resolve": {
       "version": "1.22.1",
       "resolved": "http://192.168.0.47:4873/resolve/-/resolve-1.22.1.tgz",
@@ -908,6 +1135,30 @@
         "fsevents": "~2.3.2"
       }
     },
+    "node_modules/sass": {
+      "version": "1.56.0",
+      "resolved": "http://192.168.0.47:4873/sass/-/sass-1.56.0.tgz",
+      "integrity": "sha512-WFJ9XrpkcnqZcYuLRJh5qiV6ibQOR4AezleeEjTjMsCocYW59dEG19U3fwTTXxzi2Ed3yjPBp727hbbj53pHFw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chokidar": ">=3.0.0 <4.0.0",
+        "immutable": "^4.0.0",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      },
+      "bin": {
+        "sass": "sass.js"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/select": {
+      "version": "1.1.2",
+      "resolved": "http://192.168.0.47:4873/select/-/select-1.1.2.tgz",
+      "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==",
+      "license": "MIT"
+    },
     "node_modules/source-map": {
       "version": "0.6.1",
       "resolved": "http://192.168.0.47:4873/source-map/-/source-map-0.6.1.tgz",
@@ -945,6 +1196,25 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/tiny-emitter": {
+      "version": "2.1.0",
+      "resolved": "http://192.168.0.47:4873/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
+      "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
+      "license": "MIT"
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "http://192.168.0.47:4873/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
     "node_modules/typescript": {
       "version": "4.8.4",
       "resolved": "http://192.168.0.47:4873/typescript/-/typescript-4.8.4.tgz",
@@ -959,6 +1229,15 @@
         "node": ">=4.2.0"
       }
     },
+    "node_modules/uuid": {
+      "version": "9.0.0",
+      "resolved": "http://192.168.0.47:4873/uuid/-/uuid-9.0.0.tgz",
+      "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
     "node_modules/vite": {
       "version": "3.2.0",
       "resolved": "http://192.168.0.47:4873/vite/-/vite-3.2.0.tgz",
@@ -1058,6 +1337,21 @@
       "peerDependencies": {
         "typescript": "*"
       }
+    },
+    "node_modules/vue-types": {
+      "version": "4.2.1",
+      "resolved": "http://192.168.0.47:4873/vue-types/-/vue-types-4.2.1.tgz",
+      "integrity": "sha512-DNQZmJuOvovLUIp0BENRkdnZHbI0V4e2mNvjAZOAXKD56YGvRchtUYOXA/XqTxdv7Ng5SJLZqRKRpAhm5NLaPQ==",
+      "license": "MIT",
+      "dependencies": {
+        "is-plain-object": "5.0.0"
+      },
+      "engines": {
+        "node": ">=12.16.0"
+      },
+      "peerDependencies": {
+        "vue": "^2.0.0 || ^3.0.0"
+      }
     }
   },
   "dependencies": {
@@ -1086,6 +1380,12 @@
       "integrity": "sha512-LhFTglglr63mNXUSRYD8A+ZAIu5sFqNJ4Y2fPuY7UlrySJH87rRRlhtVmMHplmfk5WkoJGmDjE9oiTfyX94CpQ==",
       "dev": true
     },
+    "@types/uuid": {
+      "version": "8.3.4",
+      "resolved": "http://192.168.0.47:4873/@types/uuid/-/uuid-8.3.4.tgz",
+      "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
+      "dev": true
+    },
     "@vitejs/plugin-vue": {
       "version": "3.2.0",
       "resolved": "http://192.168.0.47:4873/@vitejs/plugin-vue/-/plugin-vue-3.2.0.tgz",
@@ -1252,12 +1552,28 @@
       "resolved": "http://192.168.0.47:4873/@vue/shared/-/shared-3.2.41.tgz",
       "integrity": "sha512-W9mfWLHmJhkfAmV+7gDjcHeAWALQtgGT3JErxULl0oz6R6+3ug91I7IErs93eCFhPCZPHBs4QJS7YWEV7A3sxw=="
     },
+    "anymatch": {
+      "version": "3.1.2",
+      "resolved": "http://192.168.0.47:4873/anymatch/-/anymatch-3.1.2.tgz",
+      "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+      "dev": true,
+      "requires": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      }
+    },
     "balanced-match": {
       "version": "1.0.2",
       "resolved": "http://192.168.0.47:4873/balanced-match/-/balanced-match-1.0.2.tgz",
       "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
       "dev": true
     },
+    "binary-extensions": {
+      "version": "2.2.0",
+      "resolved": "http://192.168.0.47:4873/binary-extensions/-/binary-extensions-2.2.0.tgz",
+      "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+      "dev": true
+    },
     "brace-expansion": {
       "version": "2.0.1",
       "resolved": "http://192.168.0.47:4873/brace-expansion/-/brace-expansion-2.0.1.tgz",
@@ -1267,6 +1583,41 @@
         "balanced-match": "^1.0.0"
       }
     },
+    "braces": {
+      "version": "3.0.2",
+      "resolved": "http://192.168.0.47:4873/braces/-/braces-3.0.2.tgz",
+      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "dev": true,
+      "requires": {
+        "fill-range": "^7.0.1"
+      }
+    },
+    "chokidar": {
+      "version": "3.5.3",
+      "resolved": "http://192.168.0.47:4873/chokidar/-/chokidar-3.5.3.tgz",
+      "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+      "dev": true,
+      "requires": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "fsevents": "~2.3.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      }
+    },
+    "clipboard": {
+      "version": "2.0.11",
+      "resolved": "http://192.168.0.47:4873/clipboard/-/clipboard-2.0.11.tgz",
+      "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
+      "requires": {
+        "good-listener": "^1.2.2",
+        "select": "^1.1.2",
+        "tiny-emitter": "^2.0.0"
+      }
+    },
     "consola": {
       "version": "2.15.3",
       "resolved": "http://192.168.0.47:4873/consola/-/consola-2.15.3.tgz",
@@ -1283,6 +1634,11 @@
       "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
       "dev": true
     },
+    "delegate": {
+      "version": "3.2.0",
+      "resolved": "http://192.168.0.47:4873/delegate/-/delegate-3.2.0.tgz",
+      "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
+    },
     "esbuild": {
       "version": "0.15.12",
       "resolved": "http://192.168.0.47:4873/esbuild/-/esbuild-0.15.12.tgz",
@@ -1458,6 +1814,15 @@
       "resolved": "http://192.168.0.47:4873/estree-walker/-/estree-walker-2.0.2.tgz",
       "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
     },
+    "fill-range": {
+      "version": "7.0.1",
+      "resolved": "http://192.168.0.47:4873/fill-range/-/fill-range-7.0.1.tgz",
+      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "dev": true,
+      "requires": {
+        "to-regex-range": "^5.0.1"
+      }
+    },
     "fsevents": {
       "version": "2.3.2",
       "resolved": "http://192.168.0.47:4873/fsevents/-/fsevents-2.3.2.tgz",
@@ -1471,6 +1836,23 @@
       "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
       "dev": true
     },
+    "glob-parent": {
+      "version": "5.1.2",
+      "resolved": "http://192.168.0.47:4873/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "requires": {
+        "is-glob": "^4.0.1"
+      }
+    },
+    "good-listener": {
+      "version": "1.2.2",
+      "resolved": "http://192.168.0.47:4873/good-listener/-/good-listener-1.2.2.tgz",
+      "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
+      "requires": {
+        "delegate": "^3.1.2"
+      }
+    },
     "has": {
       "version": "1.0.3",
       "resolved": "http://192.168.0.47:4873/has/-/has-1.0.3.tgz",
@@ -1486,6 +1868,21 @@
       "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
       "dev": true
     },
+    "immutable": {
+      "version": "4.1.0",
+      "resolved": "http://192.168.0.47:4873/immutable/-/immutable-4.1.0.tgz",
+      "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
+      "dev": true
+    },
+    "is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "http://192.168.0.47:4873/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dev": true,
+      "requires": {
+        "binary-extensions": "^2.0.0"
+      }
+    },
     "is-core-module": {
       "version": "2.11.0",
       "resolved": "http://192.168.0.47:4873/is-core-module/-/is-core-module-2.11.0.tgz",
@@ -1495,6 +1892,32 @@
         "has": "^1.0.3"
       }
     },
+    "is-extglob": {
+      "version": "2.1.1",
+      "resolved": "http://192.168.0.47:4873/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true
+    },
+    "is-glob": {
+      "version": "4.0.3",
+      "resolved": "http://192.168.0.47:4873/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "requires": {
+        "is-extglob": "^2.1.1"
+      }
+    },
+    "is-number": {
+      "version": "7.0.0",
+      "resolved": "http://192.168.0.47:4873/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true
+    },
+    "is-plain-object": {
+      "version": "5.0.0",
+      "resolved": "http://192.168.0.47:4873/is-plain-object/-/is-plain-object-5.0.0.tgz",
+      "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="
+    },
     "magic-string": {
       "version": "0.25.9",
       "resolved": "http://192.168.0.47:4873/magic-string/-/magic-string-0.25.9.tgz",
@@ -1523,6 +1946,12 @@
       "resolved": "http://192.168.0.47:4873/nanoid/-/nanoid-3.3.4.tgz",
       "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw=="
     },
+    "normalize-path": {
+      "version": "3.0.0",
+      "resolved": "http://192.168.0.47:4873/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true
+    },
     "path-parse": {
       "version": "1.0.7",
       "resolved": "http://192.168.0.47:4873/path-parse/-/path-parse-1.0.7.tgz",
@@ -1534,6 +1963,12 @@
       "resolved": "http://192.168.0.47:4873/picocolors/-/picocolors-1.0.0.tgz",
       "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
     },
+    "picomatch": {
+      "version": "2.3.1",
+      "resolved": "http://192.168.0.47:4873/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "dev": true
+    },
     "pinia": {
       "version": "2.0.23",
       "resolved": "http://192.168.0.47:4873/pinia/-/pinia-2.0.23.tgz",
@@ -1561,6 +1996,15 @@
         "source-map-js": "^1.0.2"
       }
     },
+    "readdirp": {
+      "version": "3.6.0",
+      "resolved": "http://192.168.0.47:4873/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dev": true,
+      "requires": {
+        "picomatch": "^2.2.1"
+      }
+    },
     "resolve": {
       "version": "1.22.1",
       "resolved": "http://192.168.0.47:4873/resolve/-/resolve-1.22.1.tgz",
@@ -1581,6 +2025,22 @@
         "fsevents": "~2.3.2"
       }
     },
+    "sass": {
+      "version": "1.56.0",
+      "resolved": "http://192.168.0.47:4873/sass/-/sass-1.56.0.tgz",
+      "integrity": "sha512-WFJ9XrpkcnqZcYuLRJh5qiV6ibQOR4AezleeEjTjMsCocYW59dEG19U3fwTTXxzi2Ed3yjPBp727hbbj53pHFw==",
+      "dev": true,
+      "requires": {
+        "chokidar": ">=3.0.0 <4.0.0",
+        "immutable": "^4.0.0",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      }
+    },
+    "select": {
+      "version": "1.1.2",
+      "resolved": "http://192.168.0.47:4873/select/-/select-1.1.2.tgz",
+      "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA=="
+    },
     "source-map": {
       "version": "0.6.1",
       "resolved": "http://192.168.0.47:4873/source-map/-/source-map-0.6.1.tgz",
@@ -1602,12 +2062,31 @@
       "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
       "dev": true
     },
+    "tiny-emitter": {
+      "version": "2.1.0",
+      "resolved": "http://192.168.0.47:4873/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
+      "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
+    },
+    "to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "http://192.168.0.47:4873/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "requires": {
+        "is-number": "^7.0.0"
+      }
+    },
     "typescript": {
       "version": "4.8.4",
       "resolved": "http://192.168.0.47:4873/typescript/-/typescript-4.8.4.tgz",
       "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
       "devOptional": true
     },
+    "uuid": {
+      "version": "9.0.0",
+      "resolved": "http://192.168.0.47:4873/uuid/-/uuid-9.0.0.tgz",
+      "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
+    },
     "vite": {
       "version": "3.2.0",
       "resolved": "http://192.168.0.47:4873/vite/-/vite-3.2.0.tgz",
@@ -1659,6 +2138,14 @@
         "@volar/vue-language-core": "1.0.9",
         "@volar/vue-typescript": "1.0.9"
       }
+    },
+    "vue-types": {
+      "version": "4.2.1",
+      "resolved": "http://192.168.0.47:4873/vue-types/-/vue-types-4.2.1.tgz",
+      "integrity": "sha512-DNQZmJuOvovLUIp0BENRkdnZHbI0V4e2mNvjAZOAXKD56YGvRchtUYOXA/XqTxdv7Ng5SJLZqRKRpAhm5NLaPQ==",
+      "requires": {
+        "is-plain-object": "5.0.0"
+      }
     }
   }
 }

+ 4 - 0
package.json

@@ -9,13 +9,17 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "axios": "^1.1.3",
+    "clipboard": "^2.0.11",
     "consola": "^2.15.3",
     "pinia": "^2.0.23",
+    "uuid": "^9.0.0",
     "vue": "^3.2.41",
     "vue-types": "^4.2.1"
   },
   "devDependencies": {
     "@types/node": "^18.11.7",
+    "@types/uuid": "^8.3.4",
     "@vitejs/plugin-vue": "^3.2.0",
     "sass": "^1.55.0",
     "typescript": "^4.8.4",

+ 104 - 0
pnpm-lock.yaml

@@ -2,11 +2,15 @@ lockfileVersion: 5.4
 
 specifiers:
   '@types/node': ^18.11.7
+  '@types/uuid': ^8.3.4
   '@vitejs/plugin-vue': ^3.2.0
+  axios: ^1.1.3
+  clipboard: ^2.0.11
   consola: ^2.15.3
   pinia: ^2.0.23
   sass: ^1.55.0
   typescript: ^4.8.4
+  uuid: ^9.0.0
   vite: ^3.2.0
   vite-plugin-html-env: ^1.2.7
   vue: ^3.2.41
@@ -14,13 +18,17 @@ specifiers:
   vue-types: ^4.2.1
 
 dependencies:
+  axios: 1.1.3
+  clipboard: 2.0.11
   consola: 2.15.3
   pinia: 2.0.23_l7r24p6nevbtlimqmqcwa3ouhu
+  uuid: 9.0.0
   vue: 3.2.41
   vue-types: 4.2.1_vue@3.2.41
 
 devDependencies:
   '@types/node': 18.11.7
+  '@types/uuid': 8.3.4
   '@vitejs/plugin-vue': 3.2.0_vite@3.2.0+vue@3.2.41
   sass: 1.55.0
   typescript: 4.8.4
@@ -75,6 +83,10 @@ packages:
     resolution: {integrity: sha512-LhFTglglr63mNXUSRYD8A+ZAIu5sFqNJ4Y2fPuY7UlrySJH87rRRlhtVmMHplmfk5WkoJGmDjE9oiTfyX94CpQ==}
     dev: true
 
+  /@types/uuid/8.3.4:
+    resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
+    dev: true
+
   /@vitejs/plugin-vue/3.2.0_vite@3.2.0+vue@3.2.41:
     resolution: {integrity: sha512-E0tnaL4fr+qkdCNxJ+Xd0yM31UwMkQje76fsDVBBUCoGOUPexu2VDUYHL8P4CwV+zMvWw6nlRw19OnRKmYAJpw==}
     engines: {node: ^14.18.0 || >=16.0.0}
@@ -211,6 +223,20 @@ packages:
       picomatch: 2.3.1
     dev: true
 
+  /asynckit/0.4.0:
+    resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+    dev: false
+
+  /axios/1.1.3:
+    resolution: {integrity: sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==}
+    dependencies:
+      follow-redirects: 1.15.2
+      form-data: 4.0.0
+      proxy-from-env: 1.1.0
+    transitivePeerDependencies:
+      - debug
+    dev: false
+
   /balanced-match/1.0.2:
     resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
     dev: true
@@ -248,6 +274,21 @@ packages:
       fsevents: 2.3.2
     dev: true
 
+  /clipboard/2.0.11:
+    resolution: {integrity: sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==}
+    dependencies:
+      good-listener: 1.2.2
+      select: 1.1.2
+      tiny-emitter: 2.1.0
+    dev: false
+
+  /combined-stream/1.0.8:
+    resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+    engines: {node: '>= 0.8'}
+    dependencies:
+      delayed-stream: 1.0.0
+    dev: false
+
   /consola/2.15.3:
     resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==}
     dev: false
@@ -259,6 +300,15 @@ packages:
     resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
     dev: true
 
+  /delayed-stream/1.0.0:
+    resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+    engines: {node: '>=0.4.0'}
+    dev: false
+
+  /delegate/3.2.0:
+    resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==}
+    dev: false
+
   /esbuild-android-64/0.15.12:
     resolution: {integrity: sha512-MJKXwvPY9g0rGps0+U65HlTsM1wUs9lbjt5CU19RESqycGFDRijMDQsh68MtbzkqWSRdEtiKS1mtPzKneaAI0Q==}
     engines: {node: '>=12'}
@@ -479,6 +529,25 @@ packages:
       to-regex-range: 5.0.1
     dev: true
 
+  /follow-redirects/1.15.2:
+    resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
+    engines: {node: '>=4.0'}
+    peerDependencies:
+      debug: '*'
+    peerDependenciesMeta:
+      debug:
+        optional: true
+    dev: false
+
+  /form-data/4.0.0:
+    resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
+    engines: {node: '>= 6'}
+    dependencies:
+      asynckit: 0.4.0
+      combined-stream: 1.0.8
+      mime-types: 2.1.35
+    dev: false
+
   /fsevents/2.3.2:
     resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
     engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -498,6 +567,12 @@ packages:
       is-glob: 4.0.3
     dev: true
 
+  /good-listener/1.2.2:
+    resolution: {integrity: sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==}
+    dependencies:
+      delegate: 3.2.0
+    dev: false
+
   /has/1.0.3:
     resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==}
     engines: {node: '>= 0.4.0'}
@@ -554,6 +629,18 @@ packages:
     dependencies:
       sourcemap-codec: 1.4.8
 
+  /mime-db/1.52.0:
+    resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+    engines: {node: '>= 0.6'}
+    dev: false
+
+  /mime-types/2.1.35:
+    resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+    engines: {node: '>= 0.6'}
+    dependencies:
+      mime-db: 1.52.0
+    dev: false
+
   /minimatch/5.1.0:
     resolution: {integrity: sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==}
     engines: {node: '>=10'}
@@ -613,6 +700,10 @@ packages:
       picocolors: 1.0.0
       source-map-js: 1.0.2
 
+  /proxy-from-env/1.1.0:
+    resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+    dev: false
+
   /readdirp/3.6.0:
     resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
     engines: {node: '>=8.10.0'}
@@ -647,6 +738,10 @@ packages:
       source-map-js: 1.0.2
     dev: true
 
+  /select/1.1.2:
+    resolution: {integrity: sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==}
+    dev: false
+
   /source-map-js/1.0.2:
     resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
     engines: {node: '>=0.10.0'}
@@ -663,6 +758,10 @@ packages:
     engines: {node: '>= 0.4'}
     dev: true
 
+  /tiny-emitter/2.1.0:
+    resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==}
+    dev: false
+
   /to-fast-properties/2.0.0:
     resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
     engines: {node: '>=4'}
@@ -679,6 +778,11 @@ packages:
     engines: {node: '>=4.2.0'}
     hasBin: true
 
+  /uuid/9.0.0:
+    resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
+    hasBin: true
+    dev: false
+
   /vite-plugin-html-env/1.2.7_vite@3.2.0:
     resolution: {integrity: sha512-vdTnKtuBeB8Zp93DCbN0Qjf4odW2msVRq45r7lGKA6nwQGJFj6YemU54u3xPPkvDeZhG8DEEU64xbLwzVEBilQ==}
     engines: {node: '>=12.0.0'}

+ 24 - 2
src/App.vue

@@ -11,7 +11,7 @@ import { useAppStore } from "./store/modules/app";
 const sceneStore = useSceneStore();
 const appStore = useAppStore();
 const dataLoaded = ref(false);
-const scene$ = ref<HTMLElement | null>(null);
+const scene$ = ref<Nullable<HTMLElement>>(null);
 const refMiniMap = ref<Nullable<string>>(null);
 const flying = computed(() => appStore.flying);
 const player = computed(() => appStore.player);
@@ -36,7 +36,7 @@ watchEffect(() => {
 onMounted(async () => {
   const app = await createApp({
     dom: scene$.value as HTMLElement,
-    num: "KK-t-KwmO6julDh",
+    num: "KK-t-gpAaKxXhE9",
     mobile: true,
   });
   // SDK初始化
@@ -59,6 +59,8 @@ onMounted(async () => {
   app.use("TourPlayer");
   app.render();
   // SDK初始化结束
+
+  // SDK global Event start
   app.Scene.on("ready", () => {
     show.value = true;
   });
@@ -85,7 +87,27 @@ onMounted(async () => {
       sceneStore.loadFloorData(floors);
     }
   });
+  app.Camera.on("mode.beforeChange", ({ toMode, floorIndex, fromMode }) => {
+    appStore.setMode(toMode);
+    if (toMode != "dollhouse") {
+      appStore.setFloorId(floorIndex);
+    }
+    if (fromMode) {
+      appStore.setFlying(true);
+    }
+  });
+  app.Camera.on("mode.afterChange", () => {
+    appStore.setFlying(false);
+  });
+  app.Camera.on("flying.started", () => {
+    appStore.setFlying(true);
+  });
+  app.Camera.on("flying.ended", ({ targetPano }) => {
+    appStore.setFlying(false);
+    appStore.setFloorId(targetPano.id);
+  });
 });
+// SDK global Event end
 
 // method
 const changeMode = (name: string) => {

Разлика између датотеке није приказан због своје велике величине
+ 1557 - 0
src/components/RTC/PageRtcLive.vue


+ 592 - 0
src/components/RTC/Trtccom.vue

@@ -0,0 +1,592 @@
+<template>
+  <div class="trtccom" v-if="show">
+    <Device @switchDevice="switchDevice" @canUseDevice="canUseDevice" />
+    <div
+      class="local"
+      :class="{ disabledlocal: role == 'customer' || !videoDeviceId }"
+      id="local"
+      v-if="isJoined"
+    >
+      <div class="micBox">
+        <img
+          v-if="muteAudioLeader"
+          :src="require('@/assets/images/rtcLive/mic_off@2x.png')"
+          alt=""
+        />
+        <i v-else class="speak_mic"></i>
+      </div>
+    </div>
+
+    <template v-if="isJoined && invitedRemoteStreams.length > 0">
+      <div
+        class="local"
+        :data-role="item.userId_"
+        :class="{
+          disabledlocal:
+            item.userId_.indexOf('customer') > -1 || !videoDeviceId,
+        }"
+        v-for="item in invitedRemoteStreams"
+        :id="item.userId_"
+        :key="item.userId_"
+      >
+        <div class="micBox">
+          <img
+            v-if="muteAudioLeader"
+            :src="require('@/assets/images/rtcLive/mic_off@2x.png')"
+            alt=""
+          />
+          <i v-else class="speak_mic"></i>
+        </div>
+      </div>
+    </template>
+
+    <div
+      class="videoBox userVideo"
+      v-show="props.videoMuted || muteVideoLeader"
+      v-if="false"
+    >
+      <img
+        :src="require('@/assets/images/rtcLive/avatar_small@2x.png')"
+        alt=""
+      />
+      <div class="micBox">
+        <img
+          v-if="muteAudioLeader"
+          :src="require('@/assets/images/rtcLive/mic_off@2x.png')"
+          alt=""
+        />
+        <i v-else class="speak_mic"></i>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import TRTC, { Client, LocalStream } from "trtc-js-sdk";
+import { Dialog } from "@/global_components/";
+import {
+  ref,
+  computed,
+  watch,
+  defineEmits,
+  defineProps,
+  nextTick,
+  onUnmounted,
+} from "vue";
+import Device from "./trtc/Device";
+import { useStore } from "vuex";
+import browser from "@/utils/browser";
+// import * as apis from "@/apis/index.js";
+
+const emit = defineEmits(["audioMuted", "videoMuted", "closeSocket"]);
+
+const store = useStore();
+
+const show = ref(false);
+const invitedRemoteStreams = ref([]);
+
+const role = ref(browser.getURLParam("role"));
+
+const muteAudioLeader = ref(false);
+const muteVideoLeader = ref(false);
+
+const isJoined = computed(() => store.getters["rtc/isJoined"]);
+const isPublished = computed(() => store.getters["rtc/isPublished"]);
+const videoDeviceId = computed(() => store.getters["rtc/videoDeviceId"]);
+
+const initParamsStates = computed(
+  () =>
+    !!(
+      store.getters["rtc/sdkAppId"] &&
+      store.getters["rtc/secretKey"] &&
+      store.getters["rtc/roomId"] &&
+      store.getters["rtc/userId"]
+    )
+);
+const userSig = computed(() => store.getters["rtc/userSig"]);
+let localClient = "";
+let localStream = "";
+let shareClient = "";
+
+const props = defineProps({
+  audioMuted: {
+    default: false,
+  },
+  videoMuted: {
+    default: false,
+  },
+});
+
+watch(
+  () => props.audioMuted,
+  () => {
+    if (props.audioMuted) {
+      localStream.muteAudio();
+    } else {
+      localStream.unmuteAudio();
+    }
+    if (role.value == "leader") {
+      muteAudioLeader.value = props.audioMuted;
+    }
+  }
+);
+
+watch(
+  () => props.videoMuted,
+  () => {
+    if (props.videoMuted) {
+      localStream.muteVideo();
+    } else {
+      localStream.unmuteVideo();
+    }
+  }
+);
+
+// watch(
+//   () => isJoined.value,
+//   () => {
+//     if (!isJoined.value) {
+//       handleLeave();
+//     }
+//   }
+// );
+
+TRTC.checkSystemRequirements().then((checkResult) => {
+  if (!checkResult.result) {
+    Dialog.toast({ content: `您的设备不支持音视频通讯`, type: "error" });
+  } else {
+    show.value = true;
+  }
+});
+
+async function createLocalStream() {
+  try {
+    localStream = TRTC.createStream({
+      userId: store.getters["rtc/userId"],
+      audio: true,
+      video: false,
+      microphoneId: store.getters["rtc/audioDeviceId"],
+    });
+
+    await localStream.initialize();
+
+    if(props.audioMuted){
+       localStream.muteAudio();
+    }
+
+  } catch (error) {
+    console.log(error, "createStream");
+  }
+}
+
+async function handleJoin() {
+  if (!initParamsStates.value) {
+    return;
+  }
+
+  try {
+    // let res = await apis.getSign({ userId: store.getters["rtc/userId"] });
+    localClient = TRTC.createClient({
+      mode: "rtc",
+      sdkAppId: parseInt(store.getters["rtc/sdkAppId"], 10),
+      userId: store.getters["rtc/userId"],
+      userSig: userSig.value,
+      useStringRoomId: true,
+      enableAutoPlayDialog: false,
+    });
+    installEventHandlers();
+
+    await localClient.join({ roomId: store.getters["rtc/roomId"] });
+    store.commit("rtc/setIsJoined", true);
+    // inviteLink.value = store.commit("rtc/createShareLink");
+  } catch (error) {
+    console.error(error, "error-----------");
+  }
+
+  await createLocalStream();
+  await handlePublish();
+  localStream
+    .play("local")
+    .then(() => {
+      // addLocalControlView();
+    })
+    .catch((e) => {
+      console.log(stream);
+    });
+
+  localStream.on("error", (error) => {
+    if (error.getCode() === 0x4043) {
+      // 自动播放受限导致播放失败,此时引导用户点击页面。
+      // 在点击事件的回调函数中,执行 stream.resume();
+      Dialog.confirm({
+        showCloseIcon: false,
+        okText: "确定",
+        content:
+          "<span style='font-size: 16px; line-height: 1.5;'>在用户与网页产生交互(例如点击、触摸页面等)之前,网页将被禁止播放带有声音的媒体。点击恢复播放<span/>",
+        title: "隐私条款:",
+        single: true,
+        func: (state) => {
+          if (state == "ok") {
+            localStream.resume();
+          }
+        },
+      });
+    }
+  });
+}
+
+async function handlePublish() {
+  if (!isJoined.value) {
+    return;
+  }
+  if (isPublished.value) {
+    return;
+  }
+  if (role.value != "leader") {
+    return;
+  }
+
+  try {
+    await localClient.publish(localStream);
+    store.commit("rtc/setIsPublished", true);
+  } catch (error) {
+    console.error(error, "---------------handlePublish--------------------");
+  }
+}
+
+async function handleStartShare() {
+  shareClient = new ShareClient({
+    sdkAppId: parseInt(store.getters["rtc/sdkAppId"], 10),
+    userId: `share${store.getters["rtc/userId"]}`,
+    roomId: store.getters["rtc/roomId"],
+    secretKey: store.getters["rtc/secretKey"],
+    useStringRoomId: true,
+  });
+  try {
+    await shareClient.join();
+    await shareClient.publish();
+    console.log("Start share screen success");
+    store.isShared = true;
+  } catch (error) {
+    console.error(`Start share error: ${error.message_}`);
+  }
+}
+
+async function handleUnpublish() {
+  if (!isJoined.value) {
+    return;
+  }
+  if (!isPublished.value) {
+    return;
+  }
+  try {
+    await localClient.unpublish(localStream);
+    store.commit("rtc/setIsPublished", false);
+  } catch (error) {
+    console.error(error, "-----------handleUnpublish--------------");
+  }
+}
+
+async function handleLeave() {
+  if (isPublished.value) {
+    await handleUnpublish();
+  }
+  try {
+    uninstallEventHandlers();
+    await localClient.leave();
+    localClient.destroy();
+    localClient = null;
+    invitedRemoteStreams.value.forEach((item) => {
+      item.stop();
+    });
+    invitedRemoteStreams.value = [];
+    store.commit("rtc/setVideoDeviceId", "");
+    store.commit("rtc/setAudioDeviceId", "");
+
+    if (localStream) {
+      localStream.stop();
+      localStream.close();
+      localStream = null;
+      console.log("有执行到这里-------------");
+    }
+  } catch (error) {
+    console.error(error, "-----------handleLeave--------------");
+  }
+}
+
+function installEventHandlers() {
+  if (!localClient) {
+    return;
+  }
+  localClient.on("error", handleError);
+  localClient.on("client-banned", handleBanned);
+  localClient.on("peer-join", handlePeerJoin);
+  localClient.on("peer-leave", handlePeerLeave);
+  localClient.on("stream-added", handleStreamAdded);
+  localClient.on("stream-subscribed", handleStreamSubscribed);
+  localClient.on("stream-removed", handleStreamRemoved);
+  localClient.on("stream-updated", handleStreamUpdated);
+  localClient.on("mute-video", handleMuteVideo);
+  localClient.on("mute-audio", handleMuteAudio);
+  localClient.on("unmute-video", handleUnmuteVideo);
+  localClient.on("unmute-audio", handleUnmuteAudio);
+}
+
+function uninstallEventHandlers() {
+  if (!localClient) {
+    return;
+  }
+  localClient.off("error", handleError);
+  localClient.off("error", handleError);
+  localClient.off("client-banned", handleBanned);
+  localClient.off("peer-join", handlePeerJoin);
+  localClient.off("peer-leave", handlePeerLeave);
+  localClient.off("stream-added", handleStreamAdded);
+  localClient.off("stream-subscribed", handleStreamSubscribed);
+  localClient.off("stream-removed", handleStreamRemoved);
+  localClient.off("stream-updated", handleStreamUpdated);
+  localClient.off("mute-video", handleMuteVideo);
+  localClient.off("mute-audio", handleMuteAudio);
+  localClient.off("unmute-video", handleUnmuteVideo);
+  localClient.off("unmute-audio", handleUnmuteAudio);
+}
+
+function handleMuteVideo(event) {
+  console.log(`[${event.userId}] mute video`);
+  if (event.userId.indexOf("leader") > -1) {
+    muteVideoLeader.value = true;
+  }
+}
+
+function handleMuteAudio(event) {
+  if (event.userId.indexOf("leader") > -1) {
+    muteAudioLeader.value = true;
+  }
+  console.log(event, `[] mute audio`);
+}
+
+function handleUnmuteVideo(event) {
+  console.log(`[${event.userId}] unmute video`);
+  if (event.userId.indexOf("leader") > -1) {
+    muteVideoLeader.value = false;
+  }
+}
+
+function handleUnmuteAudio(event) {
+  console.log(`[${event.userId}] unmute audio`);
+  if (event.userId.indexOf("leader") > -1) {
+    muteAudioLeader.value = false;
+  }
+}
+
+function handleError(error) {
+  console.log(`LocalClient error: ${error.message_}`);
+}
+
+function handleBanned(error) {
+  console.log(`Client has been banned for ${error.message_}`);
+}
+
+function handlePeerJoin(event) {
+  const { userId } = event;
+  if (userId !== "local-screen") {
+    console.log(`Peer Client [${userId}] joined`);
+  }
+}
+
+function handlePeerLeave(event) {
+  const { userId } = event;
+  if (userId !== "local-screen") {
+    console.log(`[${userId}] leave`);
+  }
+}
+
+function handleStreamAdded(event) {
+  const remoteStream = event.stream;
+  const id = remoteStream.getId();
+  const userId = remoteStream.getUserId();
+
+  console.log(remoteStream, "-------------remoteStream");
+
+  if (remoteStream.getUserId() === store.getters["rtc/userId"]) {
+    // don't need to screen shared by us
+    localClient.unsubscribe(remoteStream).catch((error) => {
+      console.info(`unsubscribe failed: ${error.message_}`);
+    });
+  } else {
+    console.log(
+      `remote stream added: [${userId}] ID: ${id} type: ${remoteStream.getType()}`
+    );
+    localClient.subscribe(remoteStream).catch((error) => {
+      console.info(`subscribe failed: ${error.message_}`);
+    });
+  }
+}
+
+async function handleStreamSubscribed(event) {
+  const remoteStream = event.stream;
+
+  if (remoteStream.userId_ == store.getters["rtc/userId"]) {
+    return;
+  }
+  console.info(
+    remoteStream.userId_,
+    store.getters["rtc/userId"],
+    "handleStreamSubscribedhandleStreamSubscribed.value"
+  );
+
+  if (
+    !invitedRemoteStreams.value.some(
+      (item) => item.userId_ == remoteStream.userId_
+    )
+  ) {
+    invitedRemoteStreams.value.push(remoteStream);
+  }
+
+  console.log(invitedRemoteStreams.value, "invitedRemoteStreams.value");
+
+  await nextTick();
+  setTimeout(() => {
+    console.log(remoteStream.userId_, "remoteStream.getId()");
+    remoteStream
+      .play(remoteStream.userId_)
+      .then(() => {
+        console.log(`RemoteStream play success`, 88888888888888888888);
+      })
+      .catch((error) => {
+        console.log(`RemoteStream play failed:  error: ${error.message_}`);
+      });
+  }, 100);
+
+  // const remoteStream = event.stream;
+  // const userId = remoteStream.getUserId();
+  // console.log(`RemoteStream subscribed: [${userId}]`);
+}
+
+function handleStreamRemoved(event) {
+  const remoteStream = event.stream;
+  const userId = remoteStream.getUserId();
+  console.log(`RemoteStream removed: [${userId}]`);
+}
+
+function handleStreamUpdated(event) {
+  const remoteStream = event.stream;
+  const userId = remoteStream.getUserId();
+  console.log(
+    `RemoteStream updated: [${userId}] audio:${remoteStream.hasAudio()} video:${remoteStream.hasVideo()}`
+  );
+}
+
+let switchDevice = async ({ videoId, audioId }) => {
+  console.log()
+  if (!isJoined.value) {
+    return;
+  }
+  if (videoId) {
+    try {
+      await localStream.switchDevice("video", videoId);
+    } catch (error) {}
+  }
+  if (audioId) {
+    try {
+      await localStream.switchDevice("audio", audioId);
+    } catch (error) {}
+  }
+};
+
+onUnmounted(() => {
+  handleLeave();
+});
+
+let canUseDevice = () => {
+  console.log("可用");
+  handleJoin();
+};
+</script>
+
+<style lang="scss" scoped>
+.trtccom {
+  .local {
+    width: 70px;
+    height: 70px;
+    position: fixed;
+    z-index: 9999;
+    top: 20px;
+    left: 20px;
+    border-radius: 50%;
+    overflow: hidden;
+    background: url(~@/assets/images/rtcLive/avatar_small@2x.png) center center
+      no-repeat;
+  }
+  .videoBox {
+    width: 72px;
+    height: 72px;
+    top: 19px;
+    left: 19px;
+    position: fixed;
+    z-index: 99999;
+    border-radius: 50%;
+    overflow: hidden;
+    .loadingTip {
+      position: absolute;
+      z-index: 101;
+      left: 0;
+      top: 0;
+      bottom: 0;
+      right: 0;
+      animation: Rotate 1.5s infinite;
+      @keyframes Rotate {
+        0% {
+          transform: rotate(0deg);
+        }
+        100% {
+          transform: rotate(360deg);
+        }
+      }
+    }
+    > img {
+      width: 100%;
+      height: 100%;
+    }
+  }
+  .micBox {
+    width: 100%;
+    height: 16px;
+    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: 720px auto;
+      width: 12px;
+      height: 12px;
+      background-image: url(~@/assets/images/rtcLive/speed.png);
+      // width: 0.69rem;
+      // height: 0.69rem;
+      animation: myAnimation 3s steps(59) infinite;
+    }
+    > img {
+      width: 12px;
+    }
+  }
+  .disabledlocal {
+    opacity: 0 !important;
+    visibility: hidden !important;
+  }
+}
+
+@keyframes myAnimation {
+  0% {
+    background-position: 0px 0px;
+  }
+
+  100% {
+    background-position: -708px 0px;
+  }
+}
+</style>

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

@@ -0,0 +1,69 @@
+<template>
+  <div id="chat">
+    <div id="contents">
+      <div class="chat-item" v-for="(i, index) in chatList" :key="index">
+        <div class="chat-msg">
+          <!-- {{ user_info }} -->
+          <span :class="{ my: i.UserId == user_info.UserId }" class="chat-name"> {{ i.Nickname }} &nbsp;</span>
+          <span class="chat-content"> {{ i.text }}</span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup type="ts">
+import { onMounted, watch, defineProps, defineEmits, ref, nextTick } from "vue";
+
+const props = defineProps({
+  user_info: {
+    type: Object,
+    default: {},
+  },
+  chatList: {
+    type: Array,
+    default: [],
+  },
+});
+</script>
+
+<style scoped lang="scss">
+#chat {
+  // width: 7.03rem;
+  width: 50%;
+  max-width: 4rem;
+  max-height: 4.58rem;
+  overflow: auto;
+  position: fixed;
+  left: 0.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>

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

@@ -0,0 +1,357 @@
+<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="avatar-box" v-if="role == 'leader'" :style="`background-image:url(${avatar || defaultAvatar});`">
+          <input type="file" @change="changeFile($event)" accept=".jpg,.png" />
+          <div class="tips">更换</div>
+        </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>
+    <Cropper v-bind="option" v-if="showCrop" @close="closeCrop" @ok="confirmCrop" />
+  </div>
+</template>
+
+<script lang="ts">
+import { Dialog } from '@/global_components/';
+import browser from '@/utils/browser';
+import { useStore } from 'vuex';
+import Cropper from '@/components/cropper/cropper.vue';
+export default {
+  data() {
+    return {
+      role: browser.getURLParam('role') || 'leader',
+      mode: browser.getURLParam('mode') || 2,
+      modeList: [
+        {
+          mode: 1,
+          title: '1V1',
+        },
+        {
+          mode: 2,
+          title: '多人模式',
+        },
+      ],
+      store: useStore(),
+      userName: '',
+      roomId: browser.getURLParam('roomId'),
+      showCrop: false,
+      // base64: null,
+      defaultAvatar: require('@/assets/images/avatar_default.png'),
+      // avatar: null,
+      option: {
+        // img: 'https://4dkk.4dage.com/scene_edit_data/KK-t-SfG2Xcb8QX/user/thumb-1k.jpg?_=1661768330305',
+        img: '',
+      },
+    };
+  },
+
+  mounted() {
+   
+  },
+  computed: {
+    avatar: function () {
+      return this.$store.getters['rtc/avatar']
+    },
+  },
+  components: { Cropper },
+  // created: {},
+  // mounted:{},
+  methods: {
+    changeFile(e) {
+      let file = e.target.files[0];
+
+      let blob = window.URL.createObjectURL(file);
+      console.log(blob);
+      this.option.img = blob;
+      this.openCrop();
+      e.target.value = '';
+    },
+    confirmCrop(base64) {
+      this.$store.commit('rtc/setAvatar', base64);
+    },
+    openCrop() {
+      this.showCrop = true;
+    },
+    closeCrop() {
+      this.showCrop = false;
+    },
+    getUrl(href, queryArr) {
+      queryArr.forEach((item) => {
+        if (!browser.hasURLParam(item.key)) {
+          let ttt = href.split('index.html?');
+          href = `${ttt[0]}index.html?${item.key}=${item.val}&${ttt[1]}`;
+        } else {
+          href = browser.replaceQueryString(href, item.key, item.val);
+        }
+      });
+
+      return href;
+    },
+
+    chooseMode(mode) {
+      this.mode = mode;
+    },
+    closeCreated() {
+      this.$emit('closeCreated');
+    },
+    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',
+          },
+        ]);
+
+        // history.replaceState(null, null,hh + "&mode=" + this.mode + "&name=" + name + "&role=leader");
+        history.replaceState(null, null, tempUrl);
+        console.log(tempUrl);
+      }
+      this.store.commit('rtc/setRole', this.role);
+
+      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%;
+    }
+    .avatar-box {
+      width: 1.7067rem;
+      height: 1.7067rem;
+      margin: 0.56rem auto 0;
+      border: 1px #fff solid;
+      border-radius: 50%;
+      // background-image: url('@/assets/images/avatar_default.jpg');
+      background-size: 100%;
+      background-repeat: no-repeat;
+      position: relative;
+      overflow: hidden;
+      cursor: pointer;
+      &:hover {
+        border: 1px #ed5d18 solid;
+        .tips {
+          color: #ed5d18;
+        }
+      }
+      .tips {
+        width: 100%;
+        height: 0.5rem;
+        position: absolute;
+        background: rgba(0, 0, 0, 0.5);
+        bottom: 0;
+        left: 0;
+        text-align: center;
+        line-height: 0.5rem;
+        font-size: 0.22rem;
+      }
+      input {
+        width: 100%;
+        height: 100%;
+        opacity: 0;
+        position: relative;
+        z-index: 10;
+        cursor: pointer;
+      }
+    }
+    .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>

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

@@ -0,0 +1,143 @@
+<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 type="ts">
+import browser from "/@/utils/browser";
+import { onMounted, watch, defineProps, defineEmits, ref, computed } from "vue";
+import { useStore } from "vuex";
+const store = useStore();
+
+const emit = defineEmits(["closeDialog","confirmDialog"]);
+
+const role = ref(browser.urlHashValue("role"));
+const socket = computed(() => store.getters["rtc/socket"]);
+
+
+const props = defineProps({
+  title: {
+    type: String,
+    default: "温馨提示",
+  },
+  desc: {
+    type: String,
+    default: "是否结束带看?",
+  },
+});
+
+const endLiveCancel = () => {
+  emit("closeDialog");
+};
+
+const endLiveConfirm = () => {
+  // socket.value.emit("disconnect");
+  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 lang="ts">
+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>

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

@@ -0,0 +1,151 @@
+<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 @closeSocket="confirmDialog" @openDialog="openDialog" v-if="show"></PageRtcLive>
+    </div>
+  </transition>
+</template>
+
+<script setup>
+import PageRtcLive from './PageRtcLive';
+import { useMusicPlayer } from "@/utils/sound";
+// import Draw from "./paint/Draw";
+import createdRoom from './dialog/createdRoom';
+import dialogIndex from './dialog/index.vue';
+import dialogShare from './dialog/share.vue';
+import { Dialog } from '@/global_components/';
+import browser from '@/utils/browser';
+import { onMounted, watch, computed, ref, nextTick, watchEffect } from 'vue';
+import { useStore } from 'vuex';
+import { useApp, getApp } from '@/app';
+import { useCurrentMini } from './mini-platform'
+import { isMiniApp } from '@/env'
+import { leaveRoom } from '@/store/room'
+
+console.error('RTC create')
+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 socket = computed(() => store.getters['rtc/socket']);
+
+// const musicPlayer = useMusicPlayer();
+
+// useMusicPlayer().pause(true);
+
+const openDialog = (str, link) => {
+  shareLink.value = link;
+  dialog.value = str;
+};
+const closeDialog = (str, link) => {
+  dialog.value = '';
+  dialog.value = str;
+};
+
+const confirmDialog = async () => {
+  if (isMiniApp) {
+    useCurrentMini()?.leave()
+  }
+  await getApp().Connect.follow.exit();
+  await leaveRoom()
+  if (socket.value) {
+    if (browser.getURLParam('role') == 'leader') {
+      socket.value.emit('action', { type: 'leader-dismiss' });
+    }
+
+    setTimeout(() => {
+      socket.value.close();
+      store.commit('rtc/setSocket', null);
+    }, 0);
+  }
+
+  store.commit('rtc/setIsJoined', false);
+
+  store.commit('rtc/setVideoDeviceId', '');
+  store.commit('rtc/setAudioDeviceId', '');
+
+  let tempUrl = window.location.href;
+  ['mode', 'name', 'role','vruserId'].forEach((item) => {
+    tempUrl = browser.replaceQueryString(tempUrl, item, '');
+  });
+  // tempUrl = browser.replaceQueryString(tempUrl, 'role', 'customer')
+
+  history.replaceState(null, null, tempUrl);
+  store.commit('rtc/setRole', '');
+
+  store.commit('showShoppingguide', false);
+  dialog.value = '';
+
+  setTimeout(() => {
+    location.reload();
+  }, 100);
+};
+
+const closeCreated = (str, link) => {
+  store.commit('rtc/setAvatar', null);
+  store.commit('showShoppingguide', false);
+  showCreated.value = false;
+  //showguide();
+  if (browser.getURLParam('role')) {
+    let tempUrl = window.location.href;
+    ['mode', 'name', 'role', 'roomId', 'vruserId'].forEach((item) => {
+      tempUrl = browser.replaceQueryString(tempUrl, item, '');
+    });
+    history.replaceState(null, null, tempUrl);
+  }
+};
+
+const createdConfirm = (str, link) => {
+  showCreated.value = false;
+  show.value = true;
+  //showguide();
+};
+
+
+const showguide = () => {
+  if (!localStorage.getItem('user_guide')) {
+    Dialog.confirm({
+      showCloseIcon: false,
+      okText: '我知道了',
+      content: "<span style='font-size: 16px; line-height: 1.5;'>開發者已遵守收集、使用最終用戶個人信息有關的所有可適用法律、政策和法規,保護用戶個人信息安全。<span/>",
+      title: '隱私條款:',
+      single: true,
+      func: (state) => {
+        if (state == 'ok') {
+          localStorage.setItem('user_guide', Date.now());
+        }
+      },
+    });
+  }
+};
+
+onMounted(() => {
+  useApp().then(async (sdk) => {
+    await nextTick();
+    if (userName.value) {
+      createdConfirm();
+    } else {
+      showCreated.value = true;
+      // showguide()
+    }
+  });
+  // showCreated.value = true;
+
+});
+
+
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 79 - 0
src/components/RTC/mini-platform.js

@@ -0,0 +1,79 @@
+import { useStore } from "vuex";
+import { watchEffect, watch, computed, nextTick } from 'vue'
+import browser from "@/utils/browser";
+
+let mini
+
+export const useCurrentMini = () => mini
+
+export const initialMini = (audioMuted) => {
+  const store = useStore();
+  const socket = computed(() => store.getters["rtc/socket"]);
+
+  let isStop = false
+  const stopUpdate = (cb) => {
+    isStop = true
+    cb()
+    nextTick(() => isStop = false)
+  }
+
+
+  watch(socket, (news, olds, onCleatup) => {
+    if (!socket.value) {
+      return;
+    }
+
+    const signalActions = {
+      openAudioCallback(isOpen) {
+        stopUpdate(() => {
+          audioMuted.value = !isOpen
+          console.log('更改')
+        })
+      }
+    }
+    
+    let stopMutedWatch
+    const joinCallback = data => {
+      console.log('join room')
+      store.commit("rtc/setAvatar", browser.getURLParam("avatar"));
+      stopMutedWatch && stopMutedWatch()
+      console.log('进入监听')
+      stopMutedWatch = watch(audioMuted, (audioMuted, oldAudioMuted) => {
+        if (!isStop && audioMuted !== oldAudioMuted) {
+          console.error(audioMuted, oldAudioMuted)
+          socket.value.emit('signal', { type: 'openAudio', payload: !audioMuted })
+          if (audioMuted) {
+            console.log('静音请求')
+          } else {
+            console.log('开启声音请求')
+          }
+        }
+      }, { immediate: true, flush: 'pre' })
+    }
+    const signalCallback = data => {
+      if (data.type in signalActions) {
+        signalActions[data.type](data.payload)
+      }
+    }
+
+    socket.value.on("join", joinCallback)
+    socket.value.on('signal', signalCallback)
+
+    onCleatup(() => {
+      stopMutedWatch && stopMutedWatch()
+      socket.value.off('join', joinCallback)
+      socket.value.on('signal', signalCallback)
+    })
+  }, { immediate: true })
+
+  mini = {
+    enterShare() {
+      socket.value.emit('signal', { type: 'enterShareMode' })
+    },
+    leave() {
+      socket.value.emit('signal', { type: 'onUnload' })
+    }
+  }
+
+  return mini
+}

+ 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 lang="ts">
+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 lang="ts">
+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.ts

@@ -0,0 +1,9 @@
+import browser from "/@/utils/browser"
+import uuid from 'uuid'
+
+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 //默认不静音

+ 79 - 0
src/components/RTC/trtc/Device.vue

@@ -0,0 +1,79 @@
+<template>
+  <div></div>
+</template>
+
+<script setup>
+import { defineEmits, computed, ref } from "vue";
+import TRTC from "trtc-js-sdk";
+import { useStore } from "vuex";
+import { Dialog } from "@/global_components/";
+import browser from "@/utils/browser";
+
+const store = useStore();
+
+const videoDeviceId = computed(() => store.getters["rtc/videoDeviceId"]);
+const audioDeviceId = computed(() => store.getters["rtc/audioDeviceId"]);
+const role = ref(browser.getURLParam("role"));
+
+const emit = defineEmits(["switchDevice", "canUseDevice"]);
+
+const updateDevice = async () => {
+  console.log("updateDevice");
+  // const cameraItems = await TRTC.getCameras();
+  // cameraItems.forEach((item) => {
+  //   item.value = item.deviceId;
+  // });
+  const microphoneItems = await TRTC.getMicrophones();
+  microphoneItems.forEach((item) => {
+    item.value = item.deviceId;
+  });
+
+  store.commit("rtc/setDeviceList", {
+    cameraList: [],
+    microphoneList: microphoneItems,
+  });
+
+  // if (!videoDeviceId.value) {
+  //   if (cameraItems[0]) {
+  //     store.commit("rtc/setVideoDeviceId", cameraItems[0].deviceId);
+  //   } else {
+  //     Dialog.toast({ content: `无法获取您的摄像头权限`, type: "error" });
+  //   }
+  // }
+
+  if (!audioDeviceId.value) {
+    if (microphoneItems[0]) {
+      store.commit("rtc/setAudioDeviceId", microphoneItems[0].deviceId);
+    } else {
+      Dialog.toast({ content: `无法获取您的麦克风权限`, type: "error" });
+    }
+  }
+};
+
+let quxian = { audio: true };
+
+console.log(quxian, "--------quxian-------");
+
+navigator.mediaDevices
+  .getUserMedia(quxian)
+  .then((stream) => {
+    stream.getTracks().forEach((track) => {
+      track.stop();
+    });
+    updateDevice();
+    emit("canUseDevice", true);
+  })
+  .catch((error) => {
+    console.log(error, "error");
+    Dialog.toast({ content: `请授权您的麦克风权限`, type: "error" });
+  });
+
+navigator.mediaDevices.ondevicechange = updateDevice;
+
+const handleDeviceChange = () => {
+  emit("switchDevice", {
+    videoId: "",
+    audioId: store.audioDeviceId,
+  });
+};
+</script>

+ 50 - 6
src/components/basic/floorplan.vue

@@ -1,7 +1,7 @@
 <template>
   <div
     :class="{
-      disabled: disabled,
+      disabled: true,
       // gudieDisabled: isshoppingguide && role != 'leader',
     }"
     class="tab-layer"
@@ -13,11 +13,11 @@
         @click="changeMode('floorplan')"
       >
         <i
-        :class="{
+          :class="{
             'icon-show_plane_selected': isFloorplan,
             'icon-show_plane_normal': !isFloorplan,
           }"
-          class="iconfont ui-kankan-icon icon tip-h-center tip-v-bottom  iconzm-show_plane_normal"
+          class="iconfont ui-kankan-icon icon tip-h-center tip-v-bottom iconzm-show_plane_normal"
         ></i>
 
         <!-- <ui-icon
@@ -50,10 +50,15 @@
 </template>
 
 <script lang="ts">
-import { computed, defineComponent, ref, watchEffect } from "vue";
+import {
+  computed,
+  defineComponent,
+  ref,
+  watchEffect,
+  watch,
+  unref,
+} from "vue";
 import { propTypes } from "/@/utils/propTypes";
-const refMiniMap = ref<Nullable<string>>(null);
-const ifShow = ref(false);
 
 export default defineComponent({
   name: "floorplan",
@@ -64,12 +69,48 @@ export default defineComponent({
   },
   emits: ["changeMode", "toggleMap"],
   setup(props, { emit }) {
+    const refMiniMap = ref<Nullable<string>>(null);
+    const ifShow = ref(false);
+    const background = ref<Nullable<HTMLElement>>(null);
+    const floorplan_ref = ref<Nullable<HTMLElement>>(null);
+    const dollhouse_ref = ref<Nullable<HTMLElement>>(null);
     const isFloorplan = computed(() => props.mode === "floorplan");
     const isDollhouse = computed(() => props.mode === "dollhouse");
     watchEffect(() => {
       console.log("floorplan", isFloorplan);
     });
 
+    watch(
+      () => props.mode,
+      (val, _) => {
+        console.log(val);
+        let timer = setTimeout(() => {
+          clearTimeout(timer);
+          if (val == "floorplan") {
+            if (unref(floorplan_ref) && background.value) {
+              const width =
+                unref(floorplan_ref)?.getBoundingClientRect().width + "px";
+              const offsetLeft = unref(floorplan_ref)?.offsetLeft + "px";
+              background.value.style.width = width;
+              background.value.style.left = offsetLeft;
+            }
+          } else if (val == "dollhouse") {
+            if (unref(dollhouse_ref) && background.value) {
+              const width =
+                unref(dollhouse_ref)?.getBoundingClientRect().width + "px";
+              const offsetLeft = unref(dollhouse_ref)?.offsetLeft + "px";
+
+              background.value.style.width = width;
+              background.value.style.left = offsetLeft;
+            }
+          }
+        }, 0);
+      },
+      {
+        deep: true,
+      }
+    );
+
     const changeMode = (eventName: string) => {
       emit("changeMode", eventName);
     };
@@ -80,6 +121,9 @@ export default defineComponent({
       ifShow,
       isFloorplan,
       isDollhouse,
+      floorplan_ref,
+      background,
+      dollhouse_ref
     };
   },
 });

+ 1 - 0
src/components/basic/miniMap.vue

@@ -41,6 +41,7 @@ export default defineComponent({
       });
     });
 
+    
     const toggleMap = () => {
       isCollapse.value = !isCollapse.value;
       emit("toggleMap", isCollapse.value);

+ 8 - 0
src/hooks/userApp.ts

@@ -5,6 +5,7 @@ import consola from 'consola'
 
 const instance = (window as any).KanKan
 let app: KanKanInstance;
+let _num: string;
 const deferred = instance.Deferred()
 interface appOptions {
     region?: string
@@ -30,6 +31,9 @@ export function createApp(options: appOptions): Promise<KanKanInstance> {
         options.server = '/'
     }
     console.log('options', options)
+
+    _num = options.num
+
     if (!instance) {
         consola.error('kankan SDK 没有引入!')
     }
@@ -50,3 +54,7 @@ export function useApp(): Promise<KanKanInstance> {
 export function getApp(): KanKanInstance {
     return app
 }
+
+export function getNum(): string {
+    return _num
+}

+ 0 - 0
src/hooks/userRoom.ts


+ 3 - 0
src/store/modules/app.ts

@@ -78,6 +78,9 @@ export const useAppStore = defineStore({
         setFloorId(id: string): void {
             this.floorId = id
         },
+        setFlying(payload: boolean): void {
+            this.flying = payload
+        },
         setFloor(id: string): void {
             this.floorId = id
             getApp().Scene.gotoFloor(id)

+ 43 - 0
src/store/modules/tour.ts

@@ -0,0 +1,43 @@
+import { defineStore } from 'pinia';
+import { getApp, getNum } from '/@/hooks/userApp';
+
+
+export interface TourState {
+    toursList: any[]
+    sourceList: Nullable<any[]>
+    frameId: number
+    partId: number
+    tours: any[],
+    musicList: any[],
+    delList: any[]
+    isPlay: boolean,
+    showTours: boolean,
+}
+
+export const useTourStore = defineStore({
+    id: 'tour',
+    state: (): TourState => ({
+        toursList: [],
+        sourceList: null,
+        frameId: 0,
+        partId: 0,
+        tours: [],
+        musicList: [],
+        delList: [],
+        isPlay: false,
+        showTours: false,
+    }),
+    getters: {
+    },
+    actions: {
+        async delTours(): Promise<void> {
+            const app = getApp();
+            const res = await app.remote_editor.tour_delete({ num: getNum() })
+            console.log('res', res)
+        },
+        async delFile(): Promise<void> {
+
+        }
+    }
+
+})

Разлика између датотеке није приказан због своје велике величине
+ 415 - 0
src/utils/browser.ts


+ 144 - 0
src/utils/request.ts

@@ -0,0 +1,144 @@
+/*
+ * @Author: Rindy
+ * @Date: 2021-04-25 15:58:21
+ * @LastEditors: Rindy
+ * @LastEditTime: 2021-05-08 15:49:54
+ * @Description: 注释
+ */
+
+import axios from 'axios'
+
+// TextDecoder polyfills for lower browser
+if (undefined === window.TextEncoder) {
+    window.TextEncoder = class _TextEncoder {
+        encode(s) {
+            return unescape(encodeURIComponent(s))
+                .split('')
+                .map(function (val) {
+                    return val.charCodeAt()
+                })
+        }
+    }
+    window.TextDecoder = class _TextDecoder {
+        decode(code_arr) {
+            return decodeURIComponent(escape(String.fromCharCode.apply(null, code_arr)))
+        }
+    }
+}
+
+const fetch = axios.create()
+
+fetch.interceptors.request.use(
+    config => {
+        config.headers['token'] = 123
+
+        return config
+    },
+    error => {
+        return Promise.reject(error)
+    }
+)
+fetch.interceptors.response.use(
+    response => {
+        // 正常的文件流
+        if (!/json/gi.test(response.headers['content-type'])) {
+            return response.data
+        }
+
+        // 以文件流方式请求但是返回json,需要解析为JSON对象
+        if (response.request.responseType === 'arraybuffer') {
+            let enc = new TextDecoder('utf-8')
+            let res = JSON.parse(enc.decode(new Uint8Array(response.data)))
+            return res
+        }
+
+        return response.data
+    },
+    error => {
+        console.error(error)
+    }
+)
+
+const http = {
+    retry(func, retries = 0, delay = 1000) {
+        return new Promise((resolve, reject) => {
+            func()
+                .then(resolve)
+                .catch(error => {
+                    if (retries <= 1) {
+                        reject(error)
+                    } else {
+                        setTimeout(() => {
+                            http.retry(func, retries - 1, delay)
+                                .then(resolve)
+                                .catch(reject)
+                        }, delay)
+                    }
+                })
+        })
+    },
+    get(url,data) {
+        return fetch({
+            method: 'get',
+            url: url,
+            params:data
+        })
+    },
+    getImage(url, retries = 3) {
+        return http.retry(
+            () =>
+                new Promise((resolve, reject) => {
+                    let img = new Image()
+                    img.src = url
+                    img.crossOrigin = 'anonymous'
+                    img.onload = function () {
+                        resolve(img)
+                    }
+                    img.onerror = function () {
+                        reject(`[${url}] load fail`)
+                    }
+                }),
+            retries
+        )
+    },
+    getBueffer(url) {
+        return fetch.get(url, {
+            responseType: 'arraybuffer',
+        })
+    },
+    getBlob(url) {
+        return fetch.get(url, {
+            responseType: 'blob',
+        })
+    },
+    post(url, data) {
+        return fetch.post(url, data)
+    },
+    postFile(url, data) {
+        const form = new FormData()
+        let cb = null
+        if (data.onUploadProgress) {
+            cb = data.onUploadProgress
+            delete data.onUploadProgress
+        }
+        for (let key in data) {
+            // if (key === 'files' && data[key].length > 0) {
+            //     for (let i = 0; i < data[key].length; i++) {
+            //         form.append(key, data[key][i])
+            //     }
+            // } else {
+            //     form.append(key, data[key])
+            // }
+            form.append(key, data[key])
+        }
+
+        return fetch.post(url, form, {
+            headers: {
+                'Content-Type': 'multipart/form-data',
+            },
+            onUploadProgress: cb,
+        })
+    },
+}
+
+export { http, fetch }

+ 12 - 4
types/sdk.d.ts

@@ -6,10 +6,18 @@ declare interface CadCadManagerType {
     exit: Fn
   }
 }
+declare interface TourManagerType {
+  load: Fn<any[]>
+}
+declare interface RemoteEditorType {
+  tour_delete: ({ num: string }) => Promise<void>
+}
 
 declare interface KanKanInstance {
   [x: string]: any
   CadCadManager: CadCadManagerType
+  TourManager: TourManagerType
+  remote_editor: RemoteEditorType
 }
 
 declare interface KankanMetaDataType {
@@ -31,13 +39,13 @@ declare interface KankanMetaDataType {
     showFloorplan: number
     showVR: number
     showTour: number
-    showRule:number
+    showRule: number
   },
-  createTime:string
+  createTime: string
   version: number,
   imgVersion: number,
   linkVersion: number
-  floorPlanUser:number
+  floorPlanUser: number
   entry: any,
   sceneResolution: string,
   sceneFrom: string
@@ -45,7 +53,7 @@ declare interface KankanMetaDataType {
   boxPhotos: string,
   boxModels: string,
   videos: {
-    data: [{ blend_fov: string, id: string, value:string }],
+    data: [{ blend_fov: string, id: string, value: string }],
     upPath: string,
     version: number,
   },