Ver código fonte

新视频源

wilhelm wong 1 mês atrás
pai
commit
d39a4e2b01

+ 1 - 1
dev-dist/sw.js

@@ -79,7 +79,7 @@ define(['./workbox-f9d29cf2'], (function (workbox) { 'use strict';
    */
   workbox.precacheAndRoute([{
     "url": "index.html",
-    "revision": "0.s2vqo2svsso"
+    "revision": "0.fae4takgdgg"
   }], {});
   workbox.cleanupOutdatedCaches();
   workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

+ 158 - 158
package-lock.json

@@ -2585,11 +2585,11 @@
       }
     },
     "node_modules/@primeuix/styles": {
-      "version": "1.2.5",
-      "resolved": "https://registry.npmjs.org/@primeuix/styles/-/styles-1.2.5.tgz",
-      "integrity": "sha512-nypFRct/oaaBZqP4jinT0puW8ZIfs4u+l/vqUFmJEPU332fl5ePj6DoOpQgTLzo3OfmvSmz5a5/5b4OJJmmi7Q==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@primeuix/styles/-/styles-2.0.2.tgz",
+      "integrity": "sha512-LNtkJsTonNHF5ag+9s3+zQzm00+LRmffw68QRIHy6S/dam1JpdrrAnUzNYlWbaY7aE2EkZvQmx7Np7+PyHn+ow==",
       "dependencies": {
-        "@primeuix/styled": "^0.7.3"
+        "@primeuix/styled": "^0.7.4"
       }
     },
     "node_modules/@primeuix/utils": {
@@ -2601,12 +2601,12 @@
       }
     },
     "node_modules/@primevue/core": {
-      "version": "4.4.1",
-      "resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.4.1.tgz",
-      "integrity": "sha512-RG56iDKIJT//EtntjQzOiWOHZZJczw/qWWtdL5vFvw8/QDS9DPKn8HLpXK7N5Le6KK1MLXUsxoiGTZK+poUFUg==",
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.5.2.tgz",
+      "integrity": "sha512-1l8QeI23aEjWxOwtH5uY8BW2vHH1fxHkmbKvQaCIcX1JdS/vG8w5D6ULTGatoQAmAeVxD+2s1maGox64++7WkQ==",
       "dependencies": {
         "@primeuix/styled": "^0.7.4",
-        "@primeuix/utils": "^0.6.1"
+        "@primeuix/utils": "^0.6.2"
       },
       "engines": {
         "node": ">=12.11.0"
@@ -2616,25 +2616,25 @@
       }
     },
     "node_modules/@primevue/forms": {
-      "version": "4.4.1",
-      "resolved": "https://registry.npmjs.org/@primevue/forms/-/forms-4.4.1.tgz",
-      "integrity": "sha512-wdTVbHG6r+Msq5Cr/8r8cswWH4RdRj0eiiLYcK9S8d+nIcRnQlt/DO0o+hkgzpRVR3DvczEhodGOxUP7gkuq0g==",
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/@primevue/forms/-/forms-4.5.2.tgz",
+      "integrity": "sha512-jXOMF4IRQoBDaLY6E/Pmdh08ZQonRbyldKxQwKEybEmS/Kk+HCLOO8gw+6APoVHDRywjrulV5b3BgYol6xSEFQ==",
       "dependencies": {
         "@primeuix/forms": "^0.1.0",
-        "@primeuix/utils": "^0.6.1",
-        "@primevue/core": "4.4.1"
+        "@primeuix/utils": "^0.6.2",
+        "@primevue/core": "4.5.2"
       },
       "engines": {
         "node": ">=12.11.0"
       }
     },
     "node_modules/@primevue/icons": {
-      "version": "4.4.1",
-      "resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.4.1.tgz",
-      "integrity": "sha512-UfDimrIjVdY6EziwieyV4zPKzW6mnKHKhy4Dgyjv2oI6pNeuim+onbJo1ce22PEGXW78vfblG/3/JIzVHFweqQ==",
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.5.2.tgz",
+      "integrity": "sha512-/Z7iwXqeki87XVoi0qjApgPfZ0tpmHQfhKXHKT45GhcrcvZoyNVHeJsMz9HcNBiF53mf4BQPijtmD2L+Wt/8tw==",
       "dependencies": {
-        "@primeuix/utils": "^0.6.1",
-        "@primevue/core": "4.4.1"
+        "@primeuix/utils": "^0.6.2",
+        "@primevue/core": "4.5.2"
       },
       "engines": {
         "node": ">=12.11.0"
@@ -3157,27 +3157,27 @@
       }
     },
     "node_modules/@volar/language-core": {
-      "version": "2.4.23",
-      "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz",
-      "integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==",
+      "version": "2.4.26",
+      "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.26.tgz",
+      "integrity": "sha512-hH0SMitMxnB43OZpyF1IFPS9bgb2I3bpCh76m2WEK7BE0A0EzpYsRp0CCH2xNKshr7kacU5TQBLYn4zj7CG60A==",
       "dev": true,
       "dependencies": {
-        "@volar/source-map": "2.4.23"
+        "@volar/source-map": "2.4.26"
       }
     },
     "node_modules/@volar/source-map": {
-      "version": "2.4.23",
-      "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz",
-      "integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==",
+      "version": "2.4.26",
+      "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.26.tgz",
+      "integrity": "sha512-JJw0Tt/kSFsIRmgTQF4JSt81AUSI1aEye5Zl65EeZ8H35JHnTvFGmpDOBn5iOxd48fyGE+ZvZBp5FcgAy/1Qhw==",
       "dev": true
     },
     "node_modules/@volar/typescript": {
-      "version": "2.4.23",
-      "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.23.tgz",
-      "integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==",
+      "version": "2.4.26",
+      "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.26.tgz",
+      "integrity": "sha512-N87ecLD48Sp6zV9zID/5yuS1+5foj0DfuYGdQ6KHj/IbKvyKv1zNX6VCmnKYwtmHadEO6mFc2EKISiu3RDPAvA==",
       "dev": true,
       "dependencies": {
-        "@volar/language-core": "2.4.23",
+        "@volar/language-core": "2.4.26",
         "path-browserify": "^1.0.1",
         "vscode-uri": "^3.0.8"
       }
@@ -3234,12 +3234,12 @@
       "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
     },
     "node_modules/@vue/language-core": {
-      "version": "3.1.5",
-      "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.5.tgz",
-      "integrity": "sha512-FMcqyzWN+sYBeqRMWPGT2QY0mUasZMVIuHvmb5NT3eeqPrbHBYtCP8JWEUCDCgM+Zr62uuWY/qoeBrPrzfa78w==",
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.7.tgz",
+      "integrity": "sha512-xbJjFptmuTQD68a3/P70HDb+js61BxYvB3+/h5BflqRNV5dvwH1TZsSsTvMKwFx+QNQf0ndOvD3iih3fHXZYzQ==",
       "dev": true,
       "dependencies": {
-        "@volar/language-core": "2.4.23",
+        "@volar/language-core": "2.4.26",
         "@vue/compiler-dom": "^3.5.0",
         "@vue/shared": "^3.5.0",
         "alien-signals": "^3.0.0",
@@ -3414,9 +3414,9 @@
       }
     },
     "node_modules/alien-signals": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.0.tgz",
-      "integrity": "sha512-yufC6VpSy8tK3I0lO67pjumo5JvDQVQyr38+3OHqe6CHl1t2VZekKZ7EKKZSqk0cRmE7U7tfZbpXiKNzuc+ckg==",
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.1.tgz",
+      "integrity": "sha512-ogkIWbVrLwKtHY6oOAXaYkAxP+cTH7V5FZ5+Tm4NZFd8VDZ6uNMDrfzqctTZ42eTMCSR3ne3otpcxmqSnFfPYA==",
       "dev": true
     },
     "node_modules/ansi-regex": {
@@ -3651,9 +3651,9 @@
       "dev": true
     },
     "node_modules/baseline-browser-mapping": {
-      "version": "2.8.31",
-      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz",
-      "integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==",
+      "version": "2.9.4",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.4.tgz",
+      "integrity": "sha512-ZCQ9GEWl73BVm8bu5Fts8nt7MHdbt5vY9bP6WGnUh+r3l8M7CgfyTlwsgCbMC66BNxPr6Xoce3j66Ms5YUQTNA==",
       "dev": true,
       "bin": {
         "baseline-browser-mapping": "dist/cli.js"
@@ -3693,9 +3693,9 @@
       }
     },
     "node_modules/browserslist": {
-      "version": "4.28.0",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz",
-      "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+      "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
       "dev": true,
       "funding": [
         {
@@ -3712,11 +3712,11 @@
         }
       ],
       "dependencies": {
-        "baseline-browser-mapping": "^2.8.25",
-        "caniuse-lite": "^1.0.30001754",
-        "electron-to-chromium": "^1.5.249",
+        "baseline-browser-mapping": "^2.9.0",
+        "caniuse-lite": "^1.0.30001759",
+        "electron-to-chromium": "^1.5.263",
         "node-releases": "^2.0.27",
-        "update-browserslist-db": "^1.1.4"
+        "update-browserslist-db": "^1.2.0"
       },
       "bin": {
         "browserslist": "cli.js"
@@ -3787,9 +3787,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001757",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
-      "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==",
+      "version": "1.0.30001759",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz",
+      "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==",
       "dev": true,
       "funding": [
         {
@@ -4149,9 +4149,9 @@
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.5.260",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.260.tgz",
-      "integrity": "sha512-ov8rBoOBhVawpzdre+Cmz4FB+y66Eqrk6Gwqd8NGxuhv99GQ8XqMAr351KEkOt7gukXWDg6gJWEMKgL2RLMPtA==",
+      "version": "1.5.266",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.266.tgz",
+      "integrity": "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==",
       "dev": true
     },
     "node_modules/emoji-regex": {
@@ -5773,9 +5773,9 @@
       }
     },
     "node_modules/path-scurry/node_modules/lru-cache": {
-      "version": "11.2.2",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz",
-      "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==",
+      "version": "11.2.4",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
+      "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
       "dev": true,
       "engines": {
         "node": "20 || >=22"
@@ -6030,15 +6030,15 @@
       "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="
     },
     "node_modules/primevue": {
-      "version": "4.4.1",
-      "resolved": "https://registry.npmjs.org/primevue/-/primevue-4.4.1.tgz",
-      "integrity": "sha512-JbHBa5k30pZ7mn/z4vYBOnyt5GrR15eM3X0wa3VanonxnFLYkTEx8OMh33aU6ndWeOfi7Ef57dOL3bTH+3f4hQ==",
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/primevue/-/primevue-4.5.2.tgz",
+      "integrity": "sha512-PHnGM03FNvnOg9bZRSu5KyEgokP8i0dvPVf9O9jPVpItfaaEYEyJ21L8jGHEEhYqMtDgdOzwnkGYPOIVa2MzRg==",
       "dependencies": {
         "@primeuix/styled": "^0.7.4",
-        "@primeuix/styles": "^1.2.5",
-        "@primeuix/utils": "^0.6.1",
-        "@primevue/core": "4.4.1",
-        "@primevue/icons": "4.4.1"
+        "@primeuix/styles": "^2.0.2",
+        "@primeuix/utils": "^0.6.2",
+        "@primevue/core": "4.5.2",
+        "@primevue/icons": "4.5.2"
       },
       "engines": {
         "node": ">=12.11.0"
@@ -7287,9 +7287,9 @@
       }
     },
     "node_modules/update-browserslist-db": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
-      "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",
+      "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==",
       "dev": true,
       "funding": [
         {
@@ -7381,9 +7381,9 @@
       }
     },
     "node_modules/vite": {
-      "version": "7.2.4",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz",
-      "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
+      "version": "7.2.7",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz",
+      "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
       "dev": true,
       "dependencies": {
         "esbuild": "^0.25.0",
@@ -7455,16 +7455,16 @@
       }
     },
     "node_modules/vite-plugin-pwa": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.1.0.tgz",
-      "integrity": "sha512-VsSpdubPzXhHWVINcSx6uHRMpOHVHQcHsef1QgkOlEoaIDAlssFEW88LBq1a59BuokAhsh2kUDJbaX1bZv4Bjw==",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.2.0.tgz",
+      "integrity": "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw==",
       "dev": true,
       "dependencies": {
         "debug": "^4.3.6",
         "pretty-bytes": "^6.1.1",
         "tinyglobby": "^0.2.10",
-        "workbox-build": "^7.3.0",
-        "workbox-window": "^7.3.0"
+        "workbox-build": "^7.4.0",
+        "workbox-window": "^7.4.0"
       },
       "engines": {
         "node": ">=16.0.0"
@@ -7475,8 +7475,8 @@
       "peerDependencies": {
         "@vite-pwa/assets-generator": "^1.0.0",
         "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
-        "workbox-build": "^7.3.0",
-        "workbox-window": "^7.3.0"
+        "workbox-build": "^7.4.0",
+        "workbox-window": "^7.4.0"
       },
       "peerDependenciesMeta": {
         "@vite-pwa/assets-generator": {
@@ -7579,13 +7579,13 @@
       }
     },
     "node_modules/vue-tsc": {
-      "version": "3.1.5",
-      "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.5.tgz",
-      "integrity": "sha512-L/G9IUjOWhBU0yun89rv8fKqmKC+T0HfhrFjlIml71WpfBv9eb4E9Bev8FMbyueBIU9vxQqbd+oOsVcDa5amGw==",
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.7.tgz",
+      "integrity": "sha512-r6XlyozLXC8Z0a+r4jVyinPutG91wDtvHZuXj0U+keNc0+056jIoJINBSZI2K7Sb4YHIru0JHiqssO1cJgs+Yw==",
       "dev": true,
       "dependencies": {
-        "@volar/typescript": "2.4.23",
-        "@vue/language-core": "3.1.5"
+        "@volar/typescript": "2.4.26",
+        "@vue/language-core": "3.1.7"
       },
       "bin": {
         "vue-tsc": "bin/vue-tsc.js"
@@ -9644,11 +9644,11 @@
       }
     },
     "@primeuix/styles": {
-      "version": "1.2.5",
-      "resolved": "https://registry.npmjs.org/@primeuix/styles/-/styles-1.2.5.tgz",
-      "integrity": "sha512-nypFRct/oaaBZqP4jinT0puW8ZIfs4u+l/vqUFmJEPU332fl5ePj6DoOpQgTLzo3OfmvSmz5a5/5b4OJJmmi7Q==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@primeuix/styles/-/styles-2.0.2.tgz",
+      "integrity": "sha512-LNtkJsTonNHF5ag+9s3+zQzm00+LRmffw68QRIHy6S/dam1JpdrrAnUzNYlWbaY7aE2EkZvQmx7Np7+PyHn+ow==",
       "requires": {
-        "@primeuix/styled": "^0.7.3"
+        "@primeuix/styled": "^0.7.4"
       }
     },
     "@primeuix/utils": {
@@ -9657,31 +9657,31 @@
       "integrity": "sha512-/SLNQSKQ73WbBIsflKVqbpVjCfFYvQO3Sf1LMheXyxh8JqxO4M63dzP56wwm9OPGuCQ6MYOd2AHgZXz+g7PZcg=="
     },
     "@primevue/core": {
-      "version": "4.4.1",
-      "resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.4.1.tgz",
-      "integrity": "sha512-RG56iDKIJT//EtntjQzOiWOHZZJczw/qWWtdL5vFvw8/QDS9DPKn8HLpXK7N5Le6KK1MLXUsxoiGTZK+poUFUg==",
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.5.2.tgz",
+      "integrity": "sha512-1l8QeI23aEjWxOwtH5uY8BW2vHH1fxHkmbKvQaCIcX1JdS/vG8w5D6ULTGatoQAmAeVxD+2s1maGox64++7WkQ==",
       "requires": {
         "@primeuix/styled": "^0.7.4",
-        "@primeuix/utils": "^0.6.1"
+        "@primeuix/utils": "^0.6.2"
       }
     },
     "@primevue/forms": {
-      "version": "4.4.1",
-      "resolved": "https://registry.npmjs.org/@primevue/forms/-/forms-4.4.1.tgz",
-      "integrity": "sha512-wdTVbHG6r+Msq5Cr/8r8cswWH4RdRj0eiiLYcK9S8d+nIcRnQlt/DO0o+hkgzpRVR3DvczEhodGOxUP7gkuq0g==",
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/@primevue/forms/-/forms-4.5.2.tgz",
+      "integrity": "sha512-jXOMF4IRQoBDaLY6E/Pmdh08ZQonRbyldKxQwKEybEmS/Kk+HCLOO8gw+6APoVHDRywjrulV5b3BgYol6xSEFQ==",
       "requires": {
         "@primeuix/forms": "^0.1.0",
-        "@primeuix/utils": "^0.6.1",
-        "@primevue/core": "4.4.1"
+        "@primeuix/utils": "^0.6.2",
+        "@primevue/core": "4.5.2"
       }
     },
     "@primevue/icons": {
-      "version": "4.4.1",
-      "resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.4.1.tgz",
-      "integrity": "sha512-UfDimrIjVdY6EziwieyV4zPKzW6mnKHKhy4Dgyjv2oI6pNeuim+onbJo1ce22PEGXW78vfblG/3/JIzVHFweqQ==",
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.5.2.tgz",
+      "integrity": "sha512-/Z7iwXqeki87XVoi0qjApgPfZ0tpmHQfhKXHKT45GhcrcvZoyNVHeJsMz9HcNBiF53mf4BQPijtmD2L+Wt/8tw==",
       "requires": {
-        "@primeuix/utils": "^0.6.1",
-        "@primevue/core": "4.4.1"
+        "@primeuix/utils": "^0.6.2",
+        "@primevue/core": "4.5.2"
       }
     },
     "@rolldown/pluginutils": {
@@ -10014,27 +10014,27 @@
       }
     },
     "@volar/language-core": {
-      "version": "2.4.23",
-      "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz",
-      "integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==",
+      "version": "2.4.26",
+      "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.26.tgz",
+      "integrity": "sha512-hH0SMitMxnB43OZpyF1IFPS9bgb2I3bpCh76m2WEK7BE0A0EzpYsRp0CCH2xNKshr7kacU5TQBLYn4zj7CG60A==",
       "dev": true,
       "requires": {
-        "@volar/source-map": "2.4.23"
+        "@volar/source-map": "2.4.26"
       }
     },
     "@volar/source-map": {
-      "version": "2.4.23",
-      "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz",
-      "integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==",
+      "version": "2.4.26",
+      "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.26.tgz",
+      "integrity": "sha512-JJw0Tt/kSFsIRmgTQF4JSt81AUSI1aEye5Zl65EeZ8H35JHnTvFGmpDOBn5iOxd48fyGE+ZvZBp5FcgAy/1Qhw==",
       "dev": true
     },
     "@volar/typescript": {
-      "version": "2.4.23",
-      "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.23.tgz",
-      "integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==",
+      "version": "2.4.26",
+      "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.26.tgz",
+      "integrity": "sha512-N87ecLD48Sp6zV9zID/5yuS1+5foj0DfuYGdQ6KHj/IbKvyKv1zNX6VCmnKYwtmHadEO6mFc2EKISiu3RDPAvA==",
       "dev": true,
       "requires": {
-        "@volar/language-core": "2.4.23",
+        "@volar/language-core": "2.4.26",
         "path-browserify": "^1.0.1",
         "vscode-uri": "^3.0.8"
       }
@@ -10091,12 +10091,12 @@
       "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
     },
     "@vue/language-core": {
-      "version": "3.1.5",
-      "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.5.tgz",
-      "integrity": "sha512-FMcqyzWN+sYBeqRMWPGT2QY0mUasZMVIuHvmb5NT3eeqPrbHBYtCP8JWEUCDCgM+Zr62uuWY/qoeBrPrzfa78w==",
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.7.tgz",
+      "integrity": "sha512-xbJjFptmuTQD68a3/P70HDb+js61BxYvB3+/h5BflqRNV5dvwH1TZsSsTvMKwFx+QNQf0ndOvD3iih3fHXZYzQ==",
       "dev": true,
       "requires": {
-        "@volar/language-core": "2.4.23",
+        "@volar/language-core": "2.4.26",
         "@vue/compiler-dom": "^3.5.0",
         "@vue/shared": "^3.5.0",
         "alien-signals": "^3.0.0",
@@ -10218,9 +10218,9 @@
       }
     },
     "alien-signals": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.0.tgz",
-      "integrity": "sha512-yufC6VpSy8tK3I0lO67pjumo5JvDQVQyr38+3OHqe6CHl1t2VZekKZ7EKKZSqk0cRmE7U7tfZbpXiKNzuc+ckg==",
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.1.tgz",
+      "integrity": "sha512-ogkIWbVrLwKtHY6oOAXaYkAxP+cTH7V5FZ5+Tm4NZFd8VDZ6uNMDrfzqctTZ42eTMCSR3ne3otpcxmqSnFfPYA==",
       "dev": true
     },
     "ansi-regex": {
@@ -10383,9 +10383,9 @@
       "dev": true
     },
     "baseline-browser-mapping": {
-      "version": "2.8.31",
-      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz",
-      "integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==",
+      "version": "2.9.4",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.4.tgz",
+      "integrity": "sha512-ZCQ9GEWl73BVm8bu5Fts8nt7MHdbt5vY9bP6WGnUh+r3l8M7CgfyTlwsgCbMC66BNxPr6Xoce3j66Ms5YUQTNA==",
       "dev": true
     },
     "binary-extensions": {
@@ -10413,16 +10413,16 @@
       }
     },
     "browserslist": {
-      "version": "4.28.0",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz",
-      "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==",
+      "version": "4.28.1",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+      "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
       "dev": true,
       "requires": {
-        "baseline-browser-mapping": "^2.8.25",
-        "caniuse-lite": "^1.0.30001754",
-        "electron-to-chromium": "^1.5.249",
+        "baseline-browser-mapping": "^2.9.0",
+        "caniuse-lite": "^1.0.30001759",
+        "electron-to-chromium": "^1.5.263",
         "node-releases": "^2.0.27",
-        "update-browserslist-db": "^1.1.4"
+        "update-browserslist-db": "^1.2.0"
       }
     },
     "buffer-from": {
@@ -10469,9 +10469,9 @@
       "dev": true
     },
     "caniuse-lite": {
-      "version": "1.0.30001757",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz",
-      "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==",
+      "version": "1.0.30001759",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz",
+      "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==",
       "dev": true
     },
     "chokidar": {
@@ -10713,9 +10713,9 @@
       }
     },
     "electron-to-chromium": {
-      "version": "1.5.260",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.260.tgz",
-      "integrity": "sha512-ov8rBoOBhVawpzdre+Cmz4FB+y66Eqrk6Gwqd8NGxuhv99GQ8XqMAr351KEkOt7gukXWDg6gJWEMKgL2RLMPtA==",
+      "version": "1.5.266",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.266.tgz",
+      "integrity": "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==",
       "dev": true
     },
     "emoji-regex": {
@@ -11852,9 +11852,9 @@
       },
       "dependencies": {
         "lru-cache": {
-          "version": "11.2.2",
-          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz",
-          "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==",
+          "version": "11.2.4",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
+          "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
           "dev": true
         }
       }
@@ -11981,15 +11981,15 @@
       "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="
     },
     "primevue": {
-      "version": "4.4.1",
-      "resolved": "https://registry.npmjs.org/primevue/-/primevue-4.4.1.tgz",
-      "integrity": "sha512-JbHBa5k30pZ7mn/z4vYBOnyt5GrR15eM3X0wa3VanonxnFLYkTEx8OMh33aU6ndWeOfi7Ef57dOL3bTH+3f4hQ==",
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/primevue/-/primevue-4.5.2.tgz",
+      "integrity": "sha512-PHnGM03FNvnOg9bZRSu5KyEgokP8i0dvPVf9O9jPVpItfaaEYEyJ21L8jGHEEhYqMtDgdOzwnkGYPOIVa2MzRg==",
       "requires": {
         "@primeuix/styled": "^0.7.4",
-        "@primeuix/styles": "^1.2.5",
-        "@primeuix/utils": "^0.6.1",
-        "@primevue/core": "4.4.1",
-        "@primevue/icons": "4.4.1"
+        "@primeuix/styles": "^2.0.2",
+        "@primeuix/utils": "^0.6.2",
+        "@primevue/core": "4.5.2",
+        "@primevue/icons": "4.5.2"
       }
     },
     "process": {
@@ -12872,9 +12872,9 @@
       "dev": true
     },
     "update-browserslist-db": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
-      "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",
+      "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==",
       "dev": true,
       "requires": {
         "escalade": "^3.2.0",
@@ -12939,9 +12939,9 @@
       }
     },
     "vite": {
-      "version": "7.2.4",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz",
-      "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
+      "version": "7.2.7",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz",
+      "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
       "dev": true,
       "requires": {
         "esbuild": "^0.25.0",
@@ -12969,16 +12969,16 @@
       }
     },
     "vite-plugin-pwa": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.1.0.tgz",
-      "integrity": "sha512-VsSpdubPzXhHWVINcSx6uHRMpOHVHQcHsef1QgkOlEoaIDAlssFEW88LBq1a59BuokAhsh2kUDJbaX1bZv4Bjw==",
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.2.0.tgz",
+      "integrity": "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw==",
       "dev": true,
       "requires": {
         "debug": "^4.3.6",
         "pretty-bytes": "^6.1.1",
         "tinyglobby": "^0.2.10",
-        "workbox-build": "^7.3.0",
-        "workbox-window": "^7.3.0"
+        "workbox-build": "^7.4.0",
+        "workbox-window": "^7.4.0"
       }
     },
     "vscode-uri": {
@@ -13014,13 +13014,13 @@
       }
     },
     "vue-tsc": {
-      "version": "3.1.5",
-      "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.5.tgz",
-      "integrity": "sha512-L/G9IUjOWhBU0yun89rv8fKqmKC+T0HfhrFjlIml71WpfBv9eb4E9Bev8FMbyueBIU9vxQqbd+oOsVcDa5amGw==",
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.7.tgz",
+      "integrity": "sha512-r6XlyozLXC8Z0a+r4jVyinPutG91wDtvHZuXj0U+keNc0+056jIoJINBSZI2K7Sb4YHIru0JHiqssO1cJgs+Yw==",
       "dev": true,
       "requires": {
-        "@volar/typescript": "2.4.23",
-        "@vue/language-core": "3.1.5"
+        "@volar/typescript": "2.4.26",
+        "@vue/language-core": "3.1.7"
       }
     },
     "webidl-conversions": {

+ 163 - 11
src/components/VideoJSPlayer.vue

@@ -166,8 +166,13 @@ const processCover = async (url: string): Promise<void> => {
     loading.value = true;
     error.value = "";
 
-    // 检查是否需要解密(仅对封面进行解密)
-    if (url.includes("cover")) {
+    // 检查是否是测试版视频封面API(/api/video/image?id=xxx),不需要解密
+    if (url.includes("/api/video/image?id=")) {
+      // 测试版视频封面API直接使用,不需要解密
+      processedCoverUrl.value = url;
+    }
+    // 检查是否需要解密(仅对包含"cover"的URL进行解密)
+    else if (url.includes("cover")) {
       try {
         const decryptedData = await loader(url);
 
@@ -210,14 +215,33 @@ const processCover = async (url: string): Promise<void> => {
   }
 };
 
-// 处理视频 URL - 直接使用
+// 处理视频 URL(后端已配置代理,直接使用原始 URL)
+const processVideoUrl = (url: string): string => {
+  if (!url) return url;
+
+  // 后端已配置代理处理混合内容和 CORS 问题,直接返回原始 URL
+  // 如果 URL 是相对路径且以 /api/proxy/m3u8 开头,说明已经是代理路径,直接使用
+  // 否则返回原始 URL,让后端代理处理
+  return url;
+};
+
+// 处理视频 URL
 const processVideo = async (url: string): Promise<void> => {
   if (!url) return;
 
-  processedVideoUrl.value = url;
-  await nextTick();
-  await initVideoJSPlayer();
-  emit("videoLoaded", url);
+  try {
+    // 处理混合内容问题
+    const processedUrl = processVideoUrl(url);
+    processedVideoUrl.value = processedUrl;
+    
+    await nextTick();
+    await initVideoJSPlayer();
+    emit("videoLoaded", processedUrl);
+  } catch (err) {
+    console.error("[VideoJSPlayer] 处理视频 URL 失败:", err);
+    error.value = "视频地址处理失败,请检查网络连接";
+    emit("error", error.value);
+  }
 };
 
 // 初始化 Video.js 播放器
@@ -385,6 +409,26 @@ const initVideoJSPlayer = async (): Promise<void> => {
           videoEl.setAttribute("x-webkit-airplay", "allow");
         }
 
+        // 确保控件栏始终相对于容器定位并可见
+        const playerEl = player.value.el();
+        const controlBar = playerEl.querySelector(".vjs-control-bar");
+        if (controlBar && videoContainer.value) {
+          // 强制控件栏相对于容器定位
+          const containerRect = videoContainer.value.getBoundingClientRect();
+          const playerRect = playerEl.getBoundingClientRect();
+          
+          // 如果播放器元素小于容器,确保控件栏仍然可见
+          if (playerRect.height < containerRect.height) {
+            // 确保控件栏始终在容器底部
+            (controlBar as HTMLElement).style.position = "absolute";
+            (controlBar as HTMLElement).style.bottom = "0";
+            (controlBar as HTMLElement).style.left = "0";
+            (controlBar as HTMLElement).style.right = "0";
+            (controlBar as HTMLElement).style.width = "100%";
+            (controlBar as HTMLElement).style.zIndex = "10";
+          }
+        }
+
         // 监听播放和暂停事件,动态更新 poster
         player.value.on("play", () => {
           // 播放时隐藏 poster
@@ -442,10 +486,22 @@ const initVideoJSPlayer = async (): Promise<void> => {
         loading.value = false;
       });
 
-      player.value.on("canplay", () => {
+        player.value.on("canplay", () => {
         loading.value = false;
         error.value = "";
         emit("canplay");
+        
+        // 确保控件栏始终可见
+        const playerEl = player.value.el();
+        const controlBar = playerEl?.querySelector(".vjs-control-bar") as HTMLElement;
+        if (controlBar && videoContainer.value) {
+          controlBar.style.position = "absolute";
+          controlBar.style.bottom = "0";
+          controlBar.style.left = "0";
+          controlBar.style.right = "0";
+          controlBar.style.width = "100%";
+          controlBar.style.zIndex = "10";
+        }
       });
 
       player.value.on("play", () => {
@@ -462,7 +518,65 @@ const initVideoJSPlayer = async (): Promise<void> => {
 
       player.value.on("error", (event: any) => {
         console.error("Video.js 播放错误:", event);
-        error.value = "视频播放失败";
+        
+        const videoElement = player.value?.el()?.querySelector("video");
+        const videoError = videoElement?.error;
+        
+        let errorMessage = "视频播放失败";
+        
+        if (videoError) {
+          // 根据错误代码提供更详细的错误信息
+          // MediaError 常量值
+          const MEDIA_ERR_ABORTED = 1;
+          const MEDIA_ERR_NETWORK = 2;
+          const MEDIA_ERR_DECODE = 3;
+          const MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
+          
+          switch (videoError.code) {
+            case MEDIA_ERR_ABORTED:
+              errorMessage = "视频加载被中止";
+              break;
+            case MEDIA_ERR_NETWORK:
+              errorMessage = "网络错误,请检查网络连接";
+              // 检查是否是混合内容或 CORS 问题
+              const currentUrl = processedVideoUrl.value;
+              if (currentUrl && currentUrl.startsWith("http://") && window.location.protocol === "https:") {
+                errorMessage = "混合内容错误:HTTPS 页面无法加载 HTTP 视频,请联系管理员配置代理";
+              }
+              break;
+            case MEDIA_ERR_DECODE:
+              errorMessage = "视频解码失败";
+              break;
+            case MEDIA_ERR_SRC_NOT_SUPPORTED:
+              errorMessage = "视频格式不支持或地址无效";
+              // 检查是否是混合内容问题
+              if (processedVideoUrl.value && processedVideoUrl.value.startsWith("http://") && window.location.protocol === "https:") {
+                errorMessage = "混合内容错误:HTTPS 页面无法加载 HTTP 视频,请联系管理员配置代理";
+              }
+              break;
+            default:
+              errorMessage = `视频播放失败 (错误代码: ${videoError.code})`;
+          }
+        }
+        
+        // 检查是否是 CORS 错误
+        if (event && event.type === "error") {
+          const errorDetail = event.detail || event.error;
+          if (errorDetail && (errorDetail.message?.includes("CORS") || errorDetail.message?.includes("cross-origin"))) {
+            errorMessage = "跨域请求被阻止,请联系管理员配置 CORS";
+          }
+        }
+        
+        // 检查网络请求失败
+        const errorType = player.value?.error()?.code;
+        if (errorType === 2) { // MEDIA_ERR_NETWORK
+          const url = processedVideoUrl.value;
+          if (url && url.startsWith("http://") && window.location.protocol === "https:") {
+            errorMessage = "混合内容错误:HTTPS 页面无法加载 HTTP 视频。请使用 HTTPS 地址或配置后端代理";
+          }
+        }
+        
+        error.value = errorMessage;
         loading.value = false;
         emit("error", error.value);
         reject(event);
@@ -631,9 +745,28 @@ defineExpose({
 
 /* Video.js 自定义样式 */
 :deep(.video-js) {
-  width: 100%;
-  height: 100%;
+  width: 100% !important;
+  height: 100% !important;
   background-color: #000;
+  position: relative;
+}
+
+/* 确保视频元素填满容器 */
+:deep(.video-js video) {
+  width: 100% !important;
+  height: 100% !important;
+  object-fit: contain;
+}
+
+/* 确保控件栏始终相对于容器定位并可见 */
+:deep(.video-js .vjs-control-bar) {
+  position: absolute !important;
+  bottom: 0 !important;
+  left: 0 !important;
+  right: 0 !important;
+  width: 100% !important;
+  z-index: 10 !important;
+  display: flex !important;
 }
 
 /* Video.js poster 样式 */
@@ -720,6 +853,25 @@ defineExpose({
   width: 100% !important;
 }
 
+/* 确保控件栏在所有情况下都可见,即使视频比例不匹配 */
+:deep(.video-js.vjs-fluid) {
+  padding-top: 0 !important;
+}
+
+:deep(.video-js.vjs-fluid .vjs-tech) {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+}
+
+/* 确保控件栏不会被裁剪 */
+:deep(.video-js .vjs-control-bar) {
+  opacity: 1 !important;
+  visibility: visible !important;
+}
+
 /* iOS 设备特殊样式 */
 @supports (-webkit-touch-callout: none) {
   :deep(.video-js video) {

+ 64 - 0
src/router/index.ts

@@ -9,6 +9,7 @@ import Account from "../views/Account.vue";
 import Favorite from "../views/Favorite.vue";
 import TestVideo from "../views/TestVideo.vue";
 import Redirect from "../views/Redirect.vue";
+import PaymentRedirect from "../views/PaymentRedirect.vue";
 import { useUserStore } from "@/store/user";
 import { checkBanStatus } from "@/services/api";
 
@@ -61,6 +62,11 @@ const routes: Array<RouteRecordRaw> = [
     name: "Redirect",
     component: Redirect,
   },
+  {
+    path: "/payment-redirect",
+    name: "PaymentRedirect",
+    component: PaymentRedirect,
+  },
   {
     path: "/:pathMatch(.*)*",
     name: "NotFound",
@@ -139,6 +145,64 @@ router.beforeEach(async (to, from, next) => {
     // 检查失败不影响正常访问,继续执行
   }
 
+  // 检查是否有 loginCode 参数,如果有则自动登录
+  const loginCode = to.query.loginCode as string | undefined;
+  if (loginCode) {
+    // 如果用户已经登录,直接移除 loginCode 参数
+    if (userStore.token) {
+      console.log("用户已登录,移除 loginCode 参数");
+      const newQuery = { ...to.query };
+      delete newQuery.loginCode;
+      next({
+        path: to.path,
+        query: newQuery,
+        replace: true,
+      });
+      return;
+    }
+    
+    // 如果用户未登录,尝试通过 loginCode 登录
+    try {
+      console.log("检测到 loginCode 参数,开始自动登录");
+      await userStore.loginByCode(loginCode);
+      console.log("通过 loginCode 登录成功");
+      
+      // 登录成功后,移除 URL 中的 loginCode 参数
+      const newQuery = { ...to.query };
+      delete newQuery.loginCode;
+      
+      // 重定向到当前页面,但移除 loginCode 参数
+      next({
+        path: to.path,
+        query: newQuery,
+        replace: true,
+      });
+      return;
+    } catch (error: any) {
+      console.error("通过 loginCode 登录失败:", error);
+      // 登录失败时,移除 loginCode 参数并继续导航
+      const newQuery = { ...to.query };
+      delete newQuery.loginCode;
+      
+      // 显示错误信息(可选)
+      const errorMessage = error?.message || "登录失败";
+      if (errorMessage.includes("格式不正确")) {
+        console.error("登录码格式不正确,应为32位字母数字组合");
+      } else if (errorMessage.includes("无效") || errorMessage.includes("禁用")) {
+        console.error("登录码无效或账户已被禁用");
+      } else {
+        console.error("登录失败");
+      }
+      
+      next({
+        path: to.path,
+        query: newQuery,
+        replace: true,
+      });
+      return;
+    }
+  }
+
   // 如果是从一级域名跳转过来,且不是已经在中转页面,则先跳转到中转页面
   // 检查是否是从中转页面跳转过来的(通过检查 from.name 或 sessionStorage)
   const isFromRedirect = from.name === 'Redirect' || sessionStorage.getItem('_fromRedirect') === 'true';

+ 307 - 0
src/services/api.ts

@@ -1,6 +1,34 @@
 import axios from "axios";
 import { useUserStore } from "@/store/user";
 
+/**
+ * 根据视频ID格式判断视频源
+ * - 7位数字ID(如 "1117967")是视频源1
+ * - 其他位数或格式的ID是测试版(视频源3)
+ * @param videoId 视频ID
+ * @returns 1=视频源1,3=测试版
+ */
+export const getVideoSourceById = (videoId: string | number | undefined | null): 1 | 3 => {
+  if (!videoId) {
+    return 1; // 默认视频源1
+  }
+  
+  const idStr = String(videoId).trim();
+  
+  // 判断是否为纯数字
+  const isNumeric = /^\d+$/.test(idStr);
+  
+  if (!isNumeric) {
+    // 非纯数字,返回测试版
+    return 3;
+  }
+  
+  // 判断数字位数:7位数字是视频源1,其他位数是测试版
+  const digitCount = idStr.length;
+  
+  return digitCount === 7 ? 1 : 3;
+};
+
 const API_URL = import.meta.env.VITE_API_URL;
 const VIDEO_API_URL = import.meta.env.VITE_VIDEO_API_URL;
 
@@ -97,6 +125,16 @@ export const login = async (name: string, password: string): Promise<any> => {
   return res.data;
 };
 
+export const loginByCode = async (code: string): Promise<any> => {
+  const res = await api.post("/member/login-by-code", { code });
+  return res.data;
+};
+
+export const getLoginCode = async (): Promise<any> => {
+  const res = await api.get("/member/login-code");
+  return res.data;
+};
+
 export const register = async (
   name: string,
   password: string,
@@ -361,6 +399,275 @@ export const getVodList = async (
   return videoRequest(`/media/${plat_id}/menu/vods`, formData);
 };
 
+// 外部视频详情接口(slapibf.com)
+// 使用 Vite 代理或后端代理解决 CORS 问题
+export const getVodDetailFromExternal = async (
+  ids?: string,
+  page = 1,
+  limit = 20,
+  typeId?: number,
+  hours?: number
+): Promise<any> => {
+  try {
+    // 根据文档,参数应该是 pg 而不是 page
+    const params: Record<string, string> = {
+      ac: "detail",
+      pg: String(page),
+    };
+    
+    if (ids) {
+      params.ids = ids;
+    }
+    
+    if (typeId) {
+      params.t = String(typeId);
+    }
+    
+    if (hours) {
+      params.h = String(hours);
+    }
+    
+    // 优先使用 Vite 开发代理或后端代理
+    const isDev = import.meta.env.DEV;
+    const useProxy = isDev || API_URL; // 开发环境使用 Vite 代理,生产环境使用后端代理
+    
+    let res;
+    if (useProxy) {
+      // 通过代理访问(Vite 开发代理或后端代理)
+      const proxyPath = isDev ? '/api/proxy/external-vod' : '/proxy/external-vod';
+      res = await axios.get(proxyPath, {
+        params,
+        timeout: 10000,
+      });
+    } else {
+      // 直接访问(可能会遇到 CORS 问题)
+      res = await axios.get("https://slapibf.com/api.php/provide/vod/", {
+        params,
+        timeout: 10000,
+        headers: {
+          'Accept': 'application/json',
+        },
+      });
+    }
+    
+    return res.data;
+  } catch (error: any) {
+    console.error("获取外部视频详情失败:", error);
+    // 如果是 CORS 错误,提供更友好的提示
+    if (error.code === 'ERR_NETWORK' || error.message?.includes('CORS')) {
+      console.error("CORS 错误:请确保已配置 Vite 代理或后端代理");
+    }
+    throw error;
+  }
+};
+
+// 外部视频列表接口(slapibf.com)
+export const getVodListFromExternal = async (
+  typeId?: number,
+  page = 1,
+  keyword?: string,
+  hours?: number
+): Promise<any> => {
+  try {
+    const params: Record<string, string> = {
+      ac: "list",
+      pg: String(page),
+    };
+    
+    if (typeId) {
+      params.t = String(typeId);
+    }
+    
+    if (keyword) {
+      params.wd = keyword;
+    }
+    
+    if (hours) {
+      params.h = String(hours);
+    }
+    
+    // 优先使用 Vite 开发代理或后端代理
+    const isDev = import.meta.env.DEV;
+    const useProxy = isDev || API_URL;
+    
+    let res;
+    if (useProxy) {
+      const proxyPath = isDev ? '/api/proxy/external-vod' : '/proxy/external-vod';
+      res = await axios.get(proxyPath, {
+        params,
+        timeout: 10000,
+      });
+    } else {
+      res = await axios.get("https://slapibf.com/api.php/provide/vod/", {
+        params,
+        timeout: 10000,
+        headers: {
+          'Accept': 'application/json',
+        },
+      });
+    }
+    
+    return res.data;
+  } catch (error: any) {
+    console.error("获取外部视频列表失败:", error);
+    if (error.code === 'ERR_NETWORK' || error.message?.includes('CORS')) {
+      console.error("CORS 错误:请确保已配置 Vite 代理或后端代理");
+    }
+    throw error;
+  }
+};
+
+/**
+ * ===================== 测试版视频接口(视频源3)=====================
+ * 基础路径: /api/video
+ * 所有接口均为公开接口,无需认证
+ */
+
+// 测试版视频列表的随机关键词
+const TEST_VIDEO_KEYWORDS = ["露脸", "极品", "学生", "吃瓜","网红"];
+
+/**
+ * 获取随机关键词
+ * @returns 随机选择的关键词
+ */
+const getRandomKeyword = (): string => {
+  const randomIndex = Math.floor(Math.random() * TEST_VIDEO_KEYWORDS.length);
+  return TEST_VIDEO_KEYWORDS[randomIndex];
+};
+
+// 获取测试版视频列表
+export const getTestVideoList = async (
+  page = 1,
+  limit = 100,
+  keyword?: string
+): Promise<any> => {
+  try {
+    const params: Record<string, string> = {
+      p: String(page),
+      l: String(limit),
+    };
+    
+    // 如果提供了关键词(包括空字符串),使用提供的关键词
+    // 只有在 keyword 为 undefined 时才使用随机关键词
+    if (keyword !== undefined) {
+      params.k = keyword;
+    } else {
+      // 如果没有提供关键词,随机选择一个关键词
+      params.k = getRandomKeyword();
+    }
+    
+    const res = await api.get("/video/list", {
+      params,
+      timeout: 10000,
+    });
+    
+    return res.data;
+  } catch (error: any) {
+    console.error("获取测试版视频列表失败:", error);
+    throw error;
+  }
+};
+
+// 获取测试版视频详情
+export const getTestVideoDetail = async (id: number | string): Promise<any> => {
+  try {
+    const res = await api.get("/video/detail", {
+      params: { id: String(id) },
+      timeout: 10000,
+    });
+    
+    return res.data;
+  } catch (error: any) {
+    console.error("获取测试版视频详情失败:", error);
+    throw error;
+  }
+};
+
+// 获取测试版视频封面图片(返回二进制数据)
+export const getTestVideoImage = async (id: number | string): Promise<Blob> => {
+  try {
+    const res = await api.get("/video/image", {
+      params: { id: String(id) },
+      responseType: 'blob', // 返回二进制数据
+      timeout: 10000,
+    });
+    
+    return res.data;
+  } catch (error: any) {
+    console.error("获取测试版视频封面图片失败:", error);
+    throw error;
+  }
+};
+
+/**
+ * 转换测试版视频封面URL
+ * 根据API文档:
+ * - 列表和详情接口返回的image字段保持原始格式(base64或URL)
+ * - 如果外部API返回的是base64格式,会存入内存缓存供图片接口使用
+ * - 使用 /api/video/image?id={视频ID} 接口可以将base64数据转换为图片二进制数据返回
+ * - 图片接口优先从缓存获取base64数据,缓存未命中时从外部API获取
+ * - 图片响应设置了缓存头(1年),提高性能
+ * 
+ * 因此,无论image字段是什么格式,统一使用图片API接口,由后端自动处理转换和缓存
+ * 
+ * @param imageUrl 原始图片URL或base64字符串(可选,主要用于判断是否需要使用图片API)
+ * @param videoId 视频ID
+ * @returns 转换后的封面URL,使用当前API地址构建完整URL
+ */
+export const convertTestVideoCoverUrl = (
+  imageUrl: string | undefined | null,
+  videoId: number | string
+): string => {
+  // 如果已经是图片API格式,检查是否需要转换为当前API地址
+  if (imageUrl && imageUrl.includes('/api/video/image?id=')) {
+    // 如果是相对路径,使用当前API地址
+    if (imageUrl.startsWith('/')) {
+      return buildImageApiUrl(videoId);
+    }
+    // 如果已经是完整URL,检查是否是外部地址
+    try {
+      const url = new URL(imageUrl);
+      // 如果是外部地址(不是当前API地址),转换为当前API地址
+      if (API_URL && !url.href.startsWith(API_URL)) {
+        return buildImageApiUrl(videoId);
+      }
+    } catch {
+      // URL解析失败,使用当前API地址
+      return buildImageApiUrl(videoId);
+    }
+    return imageUrl;
+  }
+  
+  // 统一使用图片API接口,由后端自动处理:
+  // 1. base64格式自动转换为图片二进制数据
+  // 2. 从缓存获取base64数据(如果存在)
+  // 3. 缓存未命中时从外部API获取
+  // 4. 设置缓存头(1年)提高性能
+  return buildImageApiUrl(videoId);
+};
+
+/**
+ * 构建图片API的完整URL
+ * 使用与api实例相同的baseURL配置,确保请求到正确的API地址
+ * @param videoId 视频ID
+ * @returns 图片API的完整URL
+ */
+const buildImageApiUrl = (videoId: number | string): string => {
+  // 使用api实例的baseURL配置来构建URL
+  // api实例使用API_URL作为baseURL,这样可以确保请求到正确的API地址
+  const baseURL = API_URL || '/api';
+  
+  // 如果baseURL是相对路径(如 /api),则拼接相对路径
+  if (baseURL.startsWith('/')) {
+    const cleanBasePath = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
+    return `${cleanBasePath}/video/image?id=${videoId}`;
+  }
+  
+  // 如果baseURL是绝对路径(如 http://example.com/api),则拼接绝对路径
+  const cleanBaseUrl = baseURL.endsWith('/') ? baseURL.slice(0, -1) : baseURL;
+  return `${cleanBaseUrl}/video/image?id=${videoId}`;
+};
+
 // ===================== 全局请求拦截器:移除 HLS 视频相关文件的 Range 请求头 =====================
 let originalXHROpen: any = null;
 let originalXHRSetRequestHeader: any = null;

+ 28 - 0
src/store/user.ts

@@ -2,6 +2,7 @@ import { defineStore } from "pinia";
 import { ref, computed } from "vue";
 import {
   login as apiLogin,
+  loginByCode as apiLoginByCode,
   register as apiRegister,
   profile,
   newGuest,
@@ -118,6 +119,32 @@ export const useUserStore = defineStore("user", () => {
     return response;
   };
 
+  const loginByCode = async (code: string) => {
+    const response = await apiLoginByCode(code);
+    setToken(response.token);
+    setUserInfo(response.user);
+    userManuallyLoggedOut.value = false;
+    // 登录后基于新账号/团队强制刷新价格配置
+    const priceStore = usePriceStore();
+    priceStore.resetPriceConfig();
+    try {
+      await priceStore.fetchPriceConfig();
+    } catch (e) {
+      console.error("通过code登录后加载价格失败", e);
+    }
+    // 登录后获取并应用团队主题
+    try {
+      const themeData = await getTeamTheme();
+      const themeStore = useThemeStore();
+      if (themeData?.themeColor) {
+        themeStore.setTheme(themeData.themeColor as any);
+      }
+    } catch (e) {
+      console.error("通过code登录后获取团队主题失败", e);
+    }
+    return response;
+  };
+
   const register = async (
     name: string,
     password: string,
@@ -220,6 +247,7 @@ export const useUserStore = defineStore("user", () => {
     isGuestOrFreeUser,
     isVipUser,
     login,
+    loginByCode,
     register,
     logout,
     sync,

+ 124 - 0
src/utils/wechat-detector.ts

@@ -0,0 +1,124 @@
+/**
+ * 微信环境检测工具
+ * 用于检测是否在微信浏览器中,并提供跳转浏览器的功能
+ */
+
+/**
+ * 检测是否在微信环境中
+ * @returns {boolean} 是否在微信中
+ */
+export function isWechat(): boolean {
+  const ua = navigator.userAgent.toLowerCase();
+  return /micromessenger/.test(ua);
+}
+
+/**
+ * 检测平台类型
+ * @returns {object} 平台信息
+ */
+export function getPlatform() {
+  const ua = navigator.userAgent.toLowerCase();
+  return {
+    isAndroid: /android/.test(ua),
+    isIOS: /iphone|ipad|ipod/.test(ua),
+    isWechat: /micromessenger/.test(ua),
+  };
+}
+
+/**
+ * 尝试在浏览器中打开当前页面
+ * @param {string} url - 要打开的URL,默认为当前页面URL
+ * @returns {boolean} 是否成功触发跳转
+ */
+export function openInBrowser(url?: string): boolean {
+  const targetUrl = url || window.location.href;
+  const platform = getPlatform();
+
+  try {
+    if (platform.isAndroid) {
+      // 安卓可以直接尝试打开浏览器
+      // 方法1: 尝试使用 intent:// 协议(适用于微信内置浏览器)
+      try {
+        const urlObj = new URL(targetUrl);
+        const intentUrl = `intent://${urlObj.host}${urlObj.pathname}${urlObj.search}#Intent;scheme=https;package=com.android.chrome;end`;
+        
+        // 使用隐藏的 iframe 尝试跳转
+        const iframe = document.createElement('iframe');
+        iframe.style.display = 'none';
+        iframe.style.width = '1px';
+        iframe.style.height = '1px';
+        iframe.src = intentUrl;
+        document.body.appendChild(iframe);
+        
+        // 延迟移除 iframe,给跳转一些时间
+        setTimeout(() => {
+          try {
+            document.body.removeChild(iframe);
+          } catch (e) {
+            // 忽略移除失败
+          }
+        }, 1000);
+        
+        // 同时尝试直接跳转(作为备选方案)
+        setTimeout(() => {
+          window.location.href = targetUrl;
+        }, 500);
+        
+        return true;
+      } catch (e) {
+        // 如果 intent 失败,直接跳转
+        window.location.href = targetUrl;
+        return true;
+      }
+    } else if (platform.isIOS) {
+      // iOS 在微信中无法直接跳转,需要用户手动操作
+      // 这里尝试直接跳转(可能会失败,但至少尝试)
+      window.location.href = targetUrl;
+      return true;
+    } else {
+      // 其他平台直接跳转
+      window.location.href = targetUrl;
+      return true;
+    }
+  } catch (error) {
+    return false;
+  }
+}
+
+/**
+ * 复制链接到剪贴板
+ * @param {string} url - 要复制的URL,默认为当前页面URL
+ * @returns {Promise<boolean>} 是否成功复制
+ */
+export async function copyToClipboard(url?: string): Promise<boolean> {
+  const targetUrl = url || window.location.href;
+  
+  try {
+    if (navigator.clipboard && navigator.clipboard.writeText) {
+      await navigator.clipboard.writeText(targetUrl);
+      return true;
+    } else {
+      // 降级方案:使用传统的复制方法
+      const textArea = document.createElement('textarea');
+      textArea.value = targetUrl;
+      textArea.style.position = 'fixed';
+      textArea.style.left = '-999999px';
+      textArea.style.top = '-999999px';
+      document.body.appendChild(textArea);
+      textArea.focus();
+      textArea.select();
+      
+      try {
+        const successful = document.execCommand('copy');
+        document.body.removeChild(textArea);
+        return successful;
+      } catch (err) {
+        document.body.removeChild(textArea);
+        return false;
+      }
+    }
+  } catch (error) {
+    return false;
+  }
+}
+

+ 198 - 22
src/views/Home.vue

@@ -4,7 +4,7 @@
     <Banner position="top" />
 
     <!-- 动态标签栏 -->
-    <div v-if="!isSearchMode" class="relative">
+    <div class="relative">
       <div
         class="flex flex-wrap gap-1.5 transition-all duration-300"
         :class="{ 'max-h-14 overflow-hidden': !showAllTags }"
@@ -31,8 +31,25 @@
       </div>
     </div>
 
+    <!-- 视频源切换栏(已隐藏,仅保留测试版) -->
+    <!-- 如果只有一个选项,隐藏切换栏 -->
+    <!-- <div v-if="!isSearchMode && videoSourceOptions.length > 1" class="flex flex-wrap gap-1.5 items-center">
+      <span class="text-xs text-white/60 mr-1">视频源:</span>
+      <button
+        v-for="sourceOption in videoSourceOptions"
+        :key="sourceOption.value"
+        class="sort-chip"
+        :class="{
+          'sort-chip-active': videoSource === sourceOption.value,
+        }"
+        @click="selectVideoSource(sourceOption.value)"
+      >
+        {{ sourceOption.label }}
+      </button>
+    </div> -->
+
     <!-- 排序选择栏 -->
-    <div v-if="!isSearchMode" class="flex flex-wrap gap-1.5">
+    <div v-if="!isSearchMode && videoSource !== 3" class="flex flex-wrap gap-1.5">
       <button
         v-for="sortOption in sortOptions"
         :key="sortOption.value"
@@ -64,8 +81,9 @@
             :alt="video.name"
             cover-class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
           />
+          <!-- 测试版视频不显示时长 -->
           <div
-            v-if="selectedSort !== 'free'"
+            v-if="selectedSort !== 'free' && videoSource !== 3"
             class="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-2 py-1 rounded"
           >
             {{ formatDuration(video.duration) }}
@@ -77,8 +95,9 @@
           <h3 class="text-sm font-medium text-white/90 line-clamp-2 mb-1">
             {{ video.name }}
           </h3>
+          <!-- 测试版视频不显示观看次数和点赞 -->
           <p
-            v-if="selectedSort !== 'free'"
+            v-if="selectedSort !== 'free' && videoSource !== 3"
             class="text-xs text-white/50 truncate"
           >
             {{ formatNumber(video.view) }} 次观看 ·
@@ -410,7 +429,13 @@
 <script setup lang="ts">
 import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
 import { useRouter, useRoute } from "vue-router";
-import { searchVideoByTags, searchVideoByKeyword } from "@/services/api";
+import { 
+  searchVideoByTags, 
+  searchVideoByKeyword,
+  getTestVideoList,
+  convertTestVideoCoverUrl,
+  getVideoSourceById
+} from "@/services/api";
 import {
   videoMenus as fixedVideoMenus,
   getRandomInitialTag,
@@ -451,6 +476,16 @@ const allVideoMenus = fixedVideoMenus;
 const selectedMenu = ref<string>("");
 const device = generateMacAddress();
 
+// 视频源类型:1=视频源1(原接口),3=测试版(新接口)
+type VideoSource = 1 | 3;
+const videoSource = ref<VideoSource>(3); // 默认使用测试版
+
+// 视频源选项(已隐藏视频源1)
+const videoSourceOptions = [
+  // { value: 1 as VideoSource, label: "视频源1" }, // 已隐藏
+  { value: 3 as VideoSource, label: "测试版" },
+];
+
 // 视频列表数据
 const videoList = ref<any[]>([]);
 const loading = ref(false);
@@ -569,6 +604,26 @@ const selectMenu = async (hash: string) => {
   await loadVideosByTag(hash, 1);
 };
 
+// 选择视频源
+const selectVideoSource = async (source: VideoSource) => {
+  videoSource.value = source;
+  
+  // 重新查询当前显示的内容
+  if (isSearchMode.value) {
+    // 如果是搜索模式,重新执行搜索
+    await handleSearch(currentSearchKeyword.value, 1);
+  } else if (selectedSort.value === "free") {
+    // 如果是免费视频,直接显示免费视频列表
+    showFreeVideos();
+  } else if (selectedMenu.value) {
+    // 如果选择了特定菜单,按菜单查询
+    await loadVideosByTag(selectedMenu.value, 1);
+  } else {
+    // 如果没有选择菜单,查询所有视频
+    await loadVideosByTag("", 1);
+  }
+};
+
 // 选择排序方式
 const selectSort = async (sort: string) => {
   selectedSort.value = sort;
@@ -614,16 +669,17 @@ const showFreeVideos = () => {
 
 // 搜索功能
 const handleSearch = async (keyword: string, page = 1) => {
-  if (!keyword.trim()) {
-    // 如果关键词为空,清除搜索模式
+  const trimmedKeyword = keyword.trim();
+  
+  // 如果关键词为空,清除搜索模式,但仍然使用空字符串作为关键词(不使用随机关键词)
+  if (!trimmedKeyword) {
     isSearchMode.value = false;
     currentSearchKeyword.value = "";
-    loadVideosByTag("");
-    return;
+  } else {
+    isSearchMode.value = true;
+    currentSearchKeyword.value = trimmedKeyword;
   }
 
-  isSearchMode.value = true;
-  currentSearchKeyword.value = keyword.trim();
   loading.value = true;
   currentPage.value = page;
   // 清除之前的错误状态
@@ -631,6 +687,43 @@ const handleSearch = async (keyword: string, page = 1) => {
   isTimeout.value = false;
 
   try {
+    // 测试版视频源(视频源3)- 使用测试版接口搜索
+    if (videoSource.value === 3) {
+      // 传递关键词(即使是空字符串),避免使用随机关键词
+      const response = await getTestVideoList(page, pageSize.value, trimmedKeyword);
+      
+      if (response.code === 1 && response.data?.list) {
+        // 转换数据格式
+        videoList.value = response.data.list.map((video: any) => ({
+          id: String(video.id),
+          name: video.title || "",
+          cover: convertTestVideoCoverUrl(video.image, video.id),
+          m3u8: video.m3u8 || "",
+          duration: 0,
+          view: 0,
+          like: 0,
+          time: 0,
+          taginfo: [],
+        }));
+        
+        // 更新分页信息
+        totalPages.value = Math.ceil((response.data.total || 0) / pageSize.value);
+        totalCount.value = response.data.total || 0;
+        
+        // 清除错误状态
+        errorMessage.value = "";
+        isTimeout.value = false;
+      } else {
+        videoList.value = [];
+        totalPages.value = 0;
+        totalCount.value = 0;
+        errorMessage.value = response.msg || "获取视频列表失败";
+        isTimeout.value = false;
+      }
+      return;
+    }
+    
+    // 视频源1的原有逻辑
     const response = await searchVideoByKeyword(
       device,
       keyword.trim(),
@@ -681,12 +774,29 @@ const playVideo = (video: any) => {
   // 保存当前浏览状态,以便返回时恢复
   saveCurrentState();
 
+  // 构建基础查询参数
+  const baseQuery: Record<string, string> = {};
+  
+  // 根据视频ID格式判断视频源(如果当前选择的视频源与ID格式不匹配,使用ID格式判断的结果)
+  const idBasedSource = getVideoSourceById(video.id);
+  
+  // 如果当前选择的视频源与ID格式判断的结果一致,使用当前选择的视频源
+  // 否则,使用ID格式判断的结果(优先使用ID格式判断)
+  const finalSource = (videoSource.value === idBasedSource) ? videoSource.value : idBasedSource;
+  
+  // 添加视频源参数
+  if (finalSource === 3) {
+    baseQuery.source = '3';
+  }
+  // 视频源1不需要传递source参数(默认)
+
   if (selectedSort.value === "free" || video.id?.startsWith("free-")) {
     // 试看视频通过URL参数传递
     router.push({
       name: "VideoPlayer",
       params: { id: video.id },
       query: {
+        ...baseQuery,
         cover: video.cover,
         m3u8: video.m3u8,
         name: video.name,
@@ -695,23 +805,26 @@ const playVideo = (video: any) => {
     });
   } else if (!isVipUser) {
     // guest和free用户通过URL参数传递cover和m3u8,同时包含视频ID
+    // 测试版视频源也需要传递视频信息,以便在未购买时显示
     router.push({
       name: "VideoPlayer",
       params: { id: video.id },
       query: {
+        ...baseQuery,
         cover: video.cover,
         m3u8: video.m3u8,
         name: video.name,
-        duration: video.duration,
-        view: video.view,
-        like: video.like,
+        duration: String(video.duration || 0),
+        view: String(video.view || 0),
+        like: String(video.like || 0),
       },
     });
-  } else if (isVipUser) {
-    // 其他VIP用户正常调用详情接口
+  } else {
+    // VIP用户正常调用详情接口
     router.push({
       name: "VideoPlayer",
       params: { id: video.id },
+      query: baseQuery,
     });
   }
 };
@@ -725,6 +838,49 @@ const loadVideosByTag = async (tagHash: string, page = 1) => {
   isTimeout.value = false;
 
   try {
+    // 测试版视频源(视频源3)- 使用测试版接口加载视频列表
+    if (videoSource.value === 3) {
+      // 如果 tagHash 存在,找到对应的标签名作为关键词
+      // 否则传递空字符串,避免使用随机关键词
+      let keyword = "";
+      if (tagHash) {
+        const tag = fixedVideoMenus.find((menu) => menu.hash === tagHash);
+        keyword = tag ? tag.name : "";
+      }
+      const response = await getTestVideoList(page, pageSize.value, keyword);
+      
+      if (response.code === 1 && response.data?.list) {
+        // 转换数据格式
+        videoList.value = response.data.list.map((video: any) => ({
+          id: String(video.id),
+          name: video.title || "",
+          cover: convertTestVideoCoverUrl(video.image, video.id),
+          m3u8: video.m3u8 || "",
+          duration: 0,
+          view: 0,
+          like: 0,
+          time: 0,
+          taginfo: [],
+        }));
+        
+        // 更新分页信息
+        totalPages.value = Math.ceil((response.data.total || 0) / pageSize.value);
+        totalCount.value = response.data.total || 0;
+        
+        // 清除错误状态
+        errorMessage.value = "";
+        isTimeout.value = false;
+      } else {
+        videoList.value = [];
+        totalPages.value = 0;
+        totalCount.value = 0;
+        errorMessage.value = response.msg || "获取视频列表失败";
+        isTimeout.value = false;
+      }
+      return;
+    }
+    
+    // 视频源1的原有逻辑
     const response = await searchVideoByTags(
       device,
       page,
@@ -785,7 +941,8 @@ const saveCurrentState = () => {
     videoList: videoList.value,
     totalPages: totalPages.value,
     totalCount: totalCount.value,
-  });
+    videoSource: videoSource.value,
+  } as any);
 };
 
 // 恢复保存的浏览状态
@@ -798,6 +955,14 @@ const restoreSavedState = async () => {
   isSearchMode.value = savedState.isSearchMode;
   currentSearchKeyword.value = savedState.currentSearchKeyword;
   currentPage.value = savedState.currentPage;
+  
+  // 恢复视频源(如果存在)
+  // 注意:如果保存的是视频源1,自动切换到测试版(视频源1已隐藏)
+  if ((savedState as any).videoSource !== undefined) {
+    const savedSource = (savedState as any).videoSource;
+    // 如果保存的是视频源1,切换到测试版
+    videoSource.value = savedSource === 1 ? 3 : savedSource;
+  }
 
   // 直接使用已保存的视频列表数据,不重新调用API
   if (savedState.videoList && savedState.videoList.length > 0) {
@@ -833,6 +998,16 @@ const restoreSavedState = async () => {
 
 // 初始化视频菜单和加载默认视频
 const initializeVideoMenus = async () => {
+  // 检查 URL 查询参数中是否有搜索关键词
+  const searchKeyword = route.query.search as string | undefined;
+  if (searchKeyword && searchKeyword.trim()) {
+    // 如果有搜索关键词,使用该关键词进行搜索(不使用默认关键词)
+    await handleSearch(searchKeyword.trim(), 1);
+    // 清除 URL 中的 search 参数,避免刷新时重复搜索
+    router.replace({ path: route.path, query: { ...route.query, search: undefined } });
+    return;
+  }
+
   if (isReturnFromVideo.value) {
     // 如果是从视频播放页返回,恢复之前的状态
     await restoreSavedState();
@@ -847,19 +1022,20 @@ const initializeVideoMenus = async () => {
       const randomTag = getRandomInitialTag();
       if (randomTag) {
         selectedMenu.value = randomTag.hash;
-        await loadVideosByTag(randomTag.hash, 1);
+        // 使用标签名作为关键词进行搜索,而不是使用随机关键词
+        await handleSearch(randomTag.name, 1);
         // 删除已使用的标签
         removeInitialTag(randomTag.hash);
         console.log(
           `首次访问,随机选择标签: ${randomTag.name} (${randomTag.hash})`
         );
       } else {
-        // 如果没有初始标签数据,加载所有视频
-        await loadVideosByTag("");
+        // 如果没有初始标签数据,使用空字符串作为关键词(不使用随机关键词)
+        await handleSearch("", 1);
       }
     } else {
-      // 不是第一次访问,加载所有视频
-      await loadVideosByTag("");
+      // 不是第一次访问,使用空字符串作为关键词(不使用随机关键词)
+      await handleSearch("", 1);
     }
   }
 };

+ 255 - 0
src/views/PaymentRedirect.vue

@@ -0,0 +1,255 @@
+<template>
+  <div class="min-h-screen bg-surface flex items-center justify-center p-4">
+    <div class="bg-surface border border-white/10 rounded-2xl p-6 w-full max-w-md text-center">
+      <!-- 微信环境提示 -->
+      <div v-if="isWechatEnv" class="space-y-6">
+        <!-- 提示图标 -->
+        <div class="mb-4">
+          <div
+            class="w-16 h-16 mx-auto bg-blue-500/20 rounded-full flex items-center justify-center"
+          >
+            <svg
+              class="w-8 h-8 text-blue-400"
+              fill="none"
+              stroke="currentColor"
+              viewBox="0 0 24 24"
+            >
+              <path
+                stroke-linecap="round"
+                stroke-linejoin="round"
+                stroke-width="2"
+                d="M12 18h.01M8 21h8a2 2 0 002-2V5a2 2 0 00-2-2H8a2 2 0 00-2 2v14a2 2 0 002 2z"
+              />
+            </svg>
+          </div>
+        </div>
+
+        <!-- 提示信息 -->
+        <h3 class="text-lg font-semibold text-white/90 mb-4">
+          请在浏览器中打开完成支付
+        </h3>
+        <div
+          class="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4 mb-6"
+        >
+          <p class="text-sm text-white/90 mb-3">
+            微信无法打开支付宝支付页面,请在浏览器中打开此链接完成支付。
+          </p>
+          <div class="text-xs text-white/70 space-y-2">
+            <p v-if="isAndroid">
+              <strong>安卓用户:</strong>点击下方按钮即可在浏览器中打开
+            </p>
+            <p v-else-if="isIOS">
+              <strong>iOS用户:</strong>请点击右上角"..."菜单,选择"在浏览器中打开",或复制链接后在浏览器中打开
+            </p>
+            <p v-else>
+              <strong>提示:</strong>请在浏览器中打开此页面完成支付
+            </p>
+          </div>
+        </div>
+
+        <!-- 操作按钮 -->
+        <div class="flex flex-col gap-2">
+          <button
+            v-if="isAndroid"
+            @click="handleOpenInBrowser"
+            class="w-full px-4 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition font-medium"
+          >
+            在浏览器中打开
+          </button>
+          <button
+            v-if="isIOS || (!isAndroid && !isIOS)"
+            @click="handleCopyLink"
+            class="w-full px-4 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition font-medium"
+          >
+            复制链接
+          </button>
+          <button
+            @click="goBack"
+            class="w-full px-4 py-2 border border-white/20 text-white/70 rounded-lg hover:bg-white/5 transition text-sm"
+          >
+            返回
+          </button>
+        </div>
+      </div>
+
+      <!-- 浏览器环境:自动跳转 -->
+      <div v-else class="space-y-6">
+        <!-- 加载动画 -->
+        <div class="mb-6">
+          <div
+            class="w-16 h-16 mx-auto border-4 border-white/20 border-t-brand rounded-full animate-spin"
+          ></div>
+        </div>
+
+        <!-- 跳转提示 -->
+        <h3 class="text-lg font-semibold text-white/90 mb-2">
+          正在跳转到支付页面...
+        </h3>
+        <p class="text-sm text-white/60">
+          如果页面没有自动跳转,请点击下方按钮
+        </p>
+
+        <!-- 手动跳转按钮 -->
+        <button
+          @click="redirectToPayment"
+          class="w-full px-4 py-3 bg-brand text-slate-900 rounded-lg hover:bg-brand/90 transition font-medium"
+        >
+          立即跳转到支付页面
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import { isWechat, getPlatform, openInBrowser, copyToClipboard } from "@/utils/wechat-detector";
+
+const route = useRoute();
+const router = useRouter();
+
+// 支付链接
+const paymentUrl = ref<string>("");
+// 真实域名URL(用于在浏览器中打开)
+const realDomainUrl = ref<string>("");
+
+// 环境检测
+const isWechatEnv = computed(() => isWechat());
+const platform = computed(() => getPlatform());
+const isAndroid = computed(() => platform.value.isAndroid);
+const isIOS = computed(() => platform.value.isIOS);
+
+// 获取真实域名并构建URL
+// 直接使用二级域名 yz1df.cc
+const buildRealDomainUrl = () => {
+  // 直接使用二级域名 yz1df.cc
+  const secondaryDomain = 'yz1df.cc';
+  const domainWithProtocol = `https://${secondaryDomain}`;
+  
+  // 构建支付中转页面的完整URL(使用二级域名)
+  // 例如:https://yz1df.cc/payment-redirect?url=支付链接
+  const currentPath = route.path;
+  const currentQuery = route.query;
+  const realUrl = new URL(`${domainWithProtocol}${currentPath}`);
+  
+  // 添加所有查询参数
+  Object.keys(currentQuery).forEach(key => {
+    if (currentQuery[key]) {
+      realUrl.searchParams.set(key, String(currentQuery[key]));
+    }
+  });
+  
+  realDomainUrl.value = realUrl.toString();
+};
+
+// 跳转到支付页面
+const redirectToPayment = () => {
+  if (paymentUrl.value) {
+    window.location.href = paymentUrl.value;
+  }
+};
+
+// 在浏览器中打开(安卓)
+const handleOpenInBrowser = () => {
+  // 使用真实域名URL
+  const urlToOpen = realDomainUrl.value || window.location.href;
+  const success = openInBrowser(urlToOpen);
+  if (success) {
+    // 延迟关闭,给跳转一些时间
+    setTimeout(() => {
+      // 可以显示提示信息
+    }, 500);
+  } else {
+    // 如果跳转失败,提示用户手动操作
+    handleCopyLink();
+  }
+};
+
+// 复制链接(iOS)
+const handleCopyLink = async () => {
+  // 使用真实域名URL
+  const urlToCopy = realDomainUrl.value || window.location.href;
+  const success = await copyToClipboard(urlToCopy);
+  if (success) {
+    // 显示成功提示(这里可以添加 toast 提示)
+    alert("链接已复制,请在浏览器中打开");
+  } else {
+    alert("复制失败,请手动复制链接");
+  }
+};
+
+// 返回
+const goBack = () => {
+  router.back();
+};
+
+onMounted(async () => {
+  // 方案2:检测当前域名,如果不是真实域名,立即跳转到真实域名
+  const currentDomain = window.location.hostname;
+  const realDomain = 'yz1df.cc';
+  
+  if (currentDomain !== realDomain) {
+    // 当前不在真实域名下,立即跳转到真实域名
+    const realDomainUrl = `https://${realDomain}`;
+    const currentPath = route.path;
+    const currentQuery = route.query;
+    
+    // 构建真实域名的完整URL
+    const realUrl = new URL(`${realDomainUrl}${currentPath}`);
+    Object.keys(currentQuery).forEach(key => {
+      if (currentQuery[key]) {
+        realUrl.searchParams.set(key, String(currentQuery[key]));
+      }
+    });
+    
+    // 使用 replace 跳转,避免返回历史记录
+    window.location.replace(realUrl.toString());
+    return; // 立即返回,不执行后续代码
+  }
+
+  // 从 URL 参数中获取支付链接
+  const urlParam = route.query.url as string;
+  if (urlParam) {
+    // 解码支付链接
+    try {
+      paymentUrl.value = decodeURIComponent(urlParam);
+    } catch (e) {
+      paymentUrl.value = urlParam;
+    }
+  }
+
+  // 检查支付链接是否存在
+  if (!paymentUrl.value) {
+    alert("支付链接不存在,请返回重试");
+    router.back();
+    return;
+  }
+
+  // 获取真实域名并构建URL(用于在浏览器中打开)
+  buildRealDomainUrl();
+
+  // 如果不是微信环境,自动跳转到支付页面
+  if (!isWechatEnv.value) {
+    // 延迟一下,让用户看到加载动画
+    setTimeout(() => {
+      redirectToPayment();
+    }, 500);
+  } else {
+    // 在微信环境中,定期检查是否已经跳转到浏览器
+    // 如果检测到不在微信环境中了,说明用户已经在浏览器中打开,自动跳转
+    const checkInterval = setInterval(() => {
+      if (!isWechat()) {
+        clearInterval(checkInterval);
+        redirectToPayment();
+      }
+    }, 500);
+
+    // 10秒后清除定时器(避免无限检查)
+    setTimeout(() => {
+      clearInterval(checkInterval);
+    }, 10000);
+  }
+});
+</script>
+

+ 91 - 22
src/views/Purchased.vue

@@ -1,7 +1,13 @@
 <script setup lang="ts">
 import { ref, onMounted, onUnmounted } from "vue";
 import { useRouter } from "vue-router";
-import { getSinglePurchaseList, getVideoDetail } from "@/services/api";
+import { 
+  getSinglePurchaseList, 
+  getVideoDetail,
+  getTestVideoDetail,
+  convertTestVideoCoverUrl,
+  getVideoSourceById
+} from "@/services/api";
 import DomainReminderDialog from "@/components/DomainReminderDialog.vue";
 
 interface PurchaseRecord {
@@ -21,6 +27,8 @@ interface PurchaseItem {
   cover: string;
   duration: number;
   createdAt: string;
+  videoSource?: 1 | 3; // 视频源:1=视频源1,3=测试版
+  m3u8?: string; // 视频播放地址
 }
 
 const router = useRouter();
@@ -120,39 +128,71 @@ const loadPurchasedItems = async () => {
       // 并发获取每个视频的详情信息
       const videoDetails = await Promise.all(
         purchaseRecords.map(async (record) => {
-          // 处理所有后端返回的记录
+          // 根据视频ID格式判断视频源
+          // 纯数字ID是视频源1,其他格式是测试版(视频源3)
+          const videoSource: 1 | 3 = getVideoSourceById(record.resourceId);
 
           try {
-            const videoResponse = await getVideoDetail(
-              device,
-              record.resourceId
-            );
-            if (videoResponse.status === 0 && videoResponse.data) {
-              const data = videoResponse.data;
-
-              // 解密封面图片
-              let coverUrl = "";
-              const originalCover =
-                data.cover || data.pic || data.thumbnail || "";
-              if (originalCover) {
-                try {
-                  coverUrl = await decryptCover(originalCover);
-                  coverUrls.value[record.id] = coverUrl;
-                } catch (error) {
-                  console.error(`解密封面失败 (${record.resourceId}):`, error);
+            let videoData: any = null;
+            let coverUrl = "";
+
+            // 根据视频源调用不同的API
+            if (videoSource === 3) {
+              // 测试版视频源(视频源3)
+              const videoResponse = await getTestVideoDetail(record.resourceId);
+              if (videoResponse.code === 1 && videoResponse.data) {
+                const data = videoResponse.data;
+                videoData = {
+                  name: data.title || `视频 ${record.resourceId}`,
+                  cover: convertTestVideoCoverUrl(data.image, data.id),
+                  duration: 0,
+                  m3u8: data.m3u8 || "", // 保存视频播放地址
+                };
+                coverUrl = videoData.cover;
+              }
+            } else {
+              // 视频源1
+              const videoResponse = await getVideoDetail(
+                device,
+                record.resourceId
+              );
+              if (videoResponse.status === 0 && videoResponse.data) {
+                const data = videoResponse.data;
+                videoData = {
+                  name: data.name || `视频 ${record.resourceId}`,
+                  cover: data.cover || data.pic || data.thumbnail || "",
+                  duration: data.duration || 0,
+                  m3u8: data.m3u8 || "", // 保存视频播放地址
+                };
+
+                // 解密封面图片(仅视频源1需要解密)
+                const originalCover = videoData.cover;
+                if (originalCover && originalCover.includes("cover")) {
+                  try {
+                    coverUrl = await decryptCover(originalCover);
+                    coverUrls.value[record.id] = coverUrl;
+                  } catch (error) {
+                    console.error(`解密封面失败 (${record.resourceId}):`, error);
+                    coverUrl = originalCover;
+                  }
+                } else {
                   coverUrl = originalCover;
                 }
               }
+            }
 
+            if (videoData) {
               return {
                 id: record.id,
                 resourceId: record.resourceId,
-                title: data.name || `视频 ${record.resourceId}`,
+                title: videoData.name,
                 meta: "高清 · 永久观看",
                 progress: Math.round(20 + ((record.id * 13) % 70)),
                 cover: coverUrl,
-                duration: data.duration || 0,
+                duration: videoData.duration || 0,
                 createdAt: record.createdAt,
+                videoSource: videoSource,
+                m3u8: videoData.m3u8 || "", // 保存视频播放地址
               } as PurchaseItem;
             } else {
               // 如果API返回失败,也返回默认信息
@@ -165,6 +205,7 @@ const loadPurchasedItems = async () => {
                 cover: "",
                 duration: 0,
                 createdAt: record.createdAt,
+                videoSource: videoSource,
               } as PurchaseItem;
             }
           } catch (error) {
@@ -180,6 +221,7 @@ const loadPurchasedItems = async () => {
               cover: "",
               duration: 0,
               createdAt: record.createdAt,
+              videoSource: videoSource,
             } as PurchaseItem;
           }
         })
@@ -265,10 +307,37 @@ const loadMore = () => {
 
 // 点击播放视频
 const playVideo = (item: PurchaseItem) => {
-  // 跳转到视频播放页面
+  // 跳转到视频播放页面,根据视频源传递source参数
+  // 确保传递source参数,让播放页面能正确识别视频源并调用对应的接口
+  const query: Record<string, string> = {};
+  
+  // 根据视频源传递source参数
+  if (item.videoSource === 3) {
+    query.source = '3';
+  } else if (item.videoSource === 1) {
+    query.source = '1'; // 显式传递source=1,确保播放页面使用视频源1接口
+  } else {
+    // 如果没有videoSource,根据ID格式判断
+    const idBasedSource = getVideoSourceById(item.resourceId);
+    query.source = idBasedSource === 3 ? '3' : '1';
+  }
+  
+  // 已购买的视频,传递视频信息(包括m3u8),确保播放器能正常显示
+  // 即使API调用失败,也能使用URL参数中的信息
+  if (item.m3u8) {
+    query.m3u8 = item.m3u8;
+  }
+  if (item.cover) {
+    query.cover = item.cover;
+  }
+  if (item.title) {
+    query.name = item.title;
+  }
+  
   router.push({
     name: "VideoPlayer",
     params: { id: item.resourceId },
+    query: query,
   });
 };
 

+ 241 - 17
src/views/TestVideo.vue

@@ -1,12 +1,31 @@
 <template>
-  <div class="test-video-container">
+  <div class="test-video-container" :class="{ 'page-loaded': pageLoaded }">
     <div class="videos-wrapper">
+      <!-- URL 输入区域 -->
+      <div class="url-input-section">
+        <div class="input-wrapper">
+          <input
+            v-model="inputUrl"
+            type="text"
+            placeholder="请输入 HLS 视频地址(.m3u8)"
+            class="url-input"
+            @keyup.enter="loadVideo"
+          />
+          <button @click="loadVideo" class="load-button">加载</button>
+        </div>
+        <div v-if="testVideoUrl" class="current-url">
+          当前地址: <span class="url-text">{{ testVideoUrl }}</span>
+        </div>
+      </div>
+
       <!-- 上面的播放器:允许206处理(Range请求) -->
       <div class="video-section">
         <div class="video-label">上面播放器:允许206处理(Range请求)</div>
-        <div class="aspect-video video-container" ref="videoContainer1">
+        <div class="video-container" ref="videoContainer1">
           <VideoJSPlayer
+            v-if="testVideoUrl"
             ref="player1Ref"
+            :key="`player1-${testVideoUrl}`"
             :m3u8-url="testVideoUrl"
             :auto-play="false"
             :hide-error="false"
@@ -16,15 +35,20 @@
             @play="onPlayer1Play"
             @canplay="onPlayer1CanPlay"
           />
+          <div v-else class="empty-placeholder">
+            <p>请输入视频地址并点击加载</p>
+          </div>
         </div>
       </div>
 
       <!-- 下面的播放器:不允许206处理(移除Range请求头) -->
       <div class="video-section">
         <div class="video-label">下面播放器:不允许206处理(移除Range请求头)</div>
-        <div class="aspect-video video-container" ref="videoContainer2">
+        <div class="video-container" ref="videoContainer2">
           <VideoJSPlayer
+            v-if="testVideoUrl"
             ref="player2Ref"
+            :key="`player2-${testVideoUrl}`"
             :m3u8-url="testVideoUrl"
             :auto-play="false"
             :hide-error="false"
@@ -34,6 +58,9 @@
             @play="onPlayer2Play"
             @canplay="onPlayer2CanPlay"
           />
+          <div v-else class="empty-placeholder">
+            <p>请输入视频地址并点击加载</p>
+          </div>
         </div>
       </div>
     </div>
@@ -44,8 +71,16 @@
 import { ref, onMounted, onUnmounted, nextTick } from "vue";
 import VideoJSPlayer from "@/components/VideoJSPlayer.vue";
 
-// 测试视频 URL
-const testVideoUrl = "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8";
+// 输入框的 URL
+const inputUrl = ref("http://cndbt02.jxcnjd.com/20250513/S1kaJCIr/hls/index.m3u8");
+// 当前播放的测试视频 URL
+const testVideoUrl = ref<string>("");
+
+// 存储所有测试过的 URL,用于判断
+const testVideoUrls = ref<Set<string>>(new Set());
+
+// 页面加载状态
+const pageLoaded = ref(false);
 
 const videoContainer1 = ref<HTMLElement>();
 const videoContainer2 = ref<HTMLElement>();
@@ -56,10 +91,40 @@ const player2Ref = ref<InstanceType<typeof VideoJSPlayer>>();
 let originalFetchFromApi: any = null;
 let originalXHRSetRequestHeaderFromApi: any = null;
 
+// 加载视频
+const loadVideo = (): void => {
+  const url = inputUrl.value.trim();
+  if (!url) {
+    alert("请输入视频地址");
+    return;
+  }
+  
+  // 验证是否是有效的 URL
+  try {
+    new URL(url);
+  } catch (e) {
+    alert("请输入有效的 URL 地址");
+    return;
+  }
+  
+  // 更新测试视频 URL(这会触发播放器重新加载)
+  testVideoUrl.value = url;
+  testVideoUrls.value.add(url);
+  
+  // 重置 Range 标记
+  (window as any).__allowRangeForTestVideo = false;
+};
+
 // 判断URL是否与测试视频相关
 const isTestVideoUrl = (url: string): boolean => {
   if (!url) return false;
-  return url.includes("test-streams.mux.dev");
+  // 检查是否是当前测试的 URL 或其相关资源
+  for (const testUrl of testVideoUrls.value) {
+    if (url.includes(testUrl) || url.startsWith(testUrl.split('/').slice(0, -1).join('/'))) {
+      return true;
+    }
+  }
+  return false;
 };
 
 // 设置测试页面的拦截器(在全局拦截器之后执行,但可以覆盖其行为)
@@ -246,15 +311,40 @@ const setupVideoMonitoring = async (): Promise<void> => {
 };
 
 onMounted(async () => {
-  // 默认禁用Range(让全局拦截器生效)
-  (window as any).__allowRangeForTestVideo = false;
-  
-  // 设置测试页面特定的拦截器
-  setupTestPageInterceptor();
-  
-  // 等待DOM更新后监控video元素
-  await nextTick();
-  setupVideoMonitoring();
+  try {
+    // 标记页面已加载
+    pageLoaded.value = true;
+    
+    // iOS Safari 调试信息
+    const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
+    if (isIOS) {
+      console.log("[TestVideo] iOS Safari 检测到");
+      console.log("[TestVideo] User Agent:", navigator.userAgent);
+      console.log("[TestVideo] Viewport:", window.innerWidth, "x", window.innerHeight);
+    }
+    
+    // 默认禁用Range(让全局拦截器生效)
+    (window as any).__allowRangeForTestVideo = false;
+    
+    // 设置测试页面特定的拦截器
+    setupTestPageInterceptor();
+    
+    // 如果有默认 URL,延迟加载以确保 DOM 完全渲染
+    if (inputUrl.value.trim()) {
+      // 延迟一下确保页面渲染完成
+      setTimeout(() => {
+        loadVideo();
+      }, 100);
+    }
+    
+    // 等待DOM更新后监控video元素
+    await nextTick();
+    setupVideoMonitoring();
+  } catch (error) {
+    console.error("TestVideo 页面初始化错误:", error);
+    // 确保页面至少能显示基本内容
+    pageLoaded.value = true;
+  }
 });
 
 onUnmounted(() => {
@@ -266,11 +356,20 @@ onUnmounted(() => {
 <style scoped>
 .test-video-container {
   min-height: 100vh;
+  min-height: -webkit-fill-available; /* iOS Safari 兼容 */
   display: flex;
-  align-items: center;
+  align-items: flex-start;
   justify-content: center;
   padding: 20px;
+  padding-top: max(20px, env(safe-area-inset-top)); /* iOS 安全区域 */
   background-color: #000;
+  box-sizing: border-box;
+  opacity: 0;
+  transition: opacity 0.3s ease-in;
+}
+
+.test-video-container.page-loaded {
+  opacity: 1;
 }
 
 .videos-wrapper {
@@ -279,7 +378,14 @@ onUnmounted(() => {
   margin: 0 auto;
   display: flex;
   flex-direction: column;
-  gap: 40px;
+  gap: 20px;
+  box-sizing: border-box;
+}
+
+@media (min-width: 768px) {
+  .videos-wrapper {
+    gap: 40px;
+  }
 }
 
 .video-section {
@@ -299,8 +405,126 @@ onUnmounted(() => {
 
 .video-container {
   width: 100%;
+  position: relative;
+  padding-bottom: 56.25%; /* 16:9 宽高比 */
+  height: 0;
   background-color: #000;
   border-radius: 8px;
   overflow: hidden;
 }
+
+.video-container > * {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+}
+
+.url-input-section {
+  width: 100%;
+  margin-bottom: 20px;
+}
+
+.input-wrapper {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  margin-bottom: 12px;
+}
+
+@media (min-width: 640px) {
+  .input-wrapper {
+    flex-direction: row;
+  }
+}
+
+.url-input {
+  flex: 1;
+  padding: 12px 16px;
+  font-size: 16px; /* iOS Safari 防止自动缩放 */
+  color: #fff;
+  background-color: rgba(255, 255, 255, 0.1);
+  border: 1px solid rgba(255, 255, 255, 0.2);
+  border-radius: 6px;
+  outline: none;
+  transition: all 0.3s;
+  -webkit-appearance: none; /* iOS Safari 移除默认样式 */
+  appearance: none;
+  box-sizing: border-box;
+  width: 100%;
+}
+
+.url-input:focus {
+  background-color: rgba(255, 255, 255, 0.15);
+  border-color: rgba(255, 255, 255, 0.4);
+}
+
+.url-input::placeholder {
+  color: rgba(255, 255, 255, 0.5);
+}
+
+.load-button {
+  padding: 12px 24px;
+  font-size: 16px; /* iOS Safari 防止自动缩放 */
+  font-weight: 500;
+  color: #fff;
+  background-color: #007bff;
+  border: none;
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.3s;
+  white-space: nowrap;
+  -webkit-appearance: none; /* iOS Safari 移除默认样式 */
+  appearance: none;
+  touch-action: manipulation; /* 改善移动端触摸响应 */
+  box-sizing: border-box;
+  width: 100%;
+}
+
+@media (min-width: 640px) {
+  .load-button {
+    width: auto;
+  }
+}
+
+.load-button:hover {
+  background-color: #0056b3;
+}
+
+.load-button:active {
+  transform: scale(0.98);
+}
+
+.current-url {
+  padding: 8px 12px;
+  font-size: 12px;
+  color: rgba(255, 255, 255, 0.7);
+  background-color: rgba(255, 255, 255, 0.05);
+  border-radius: 4px;
+  word-break: break-all;
+}
+
+.url-text {
+  color: rgba(255, 255, 255, 0.9);
+  font-family: monospace;
+}
+
+.empty-placeholder {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+  min-height: 300px;
+  color: rgba(255, 255, 255, 0.5);
+  font-size: 14px;
+  text-align: center;
+  padding: 20px;
+  box-sizing: border-box;
+}
+
+.empty-placeholder p {
+  margin: 0;
+}
 </style>

Diferenças do arquivo suprimidas por serem muito extensas
+ 843 - 34
src/views/VideoPlayer.vue


+ 18 - 0
vite.config.ts

@@ -82,6 +82,24 @@ export default defineConfig({
     host: "0.0.0.0",
     open: true,
     cors: true,
+    proxy: {
+      '/api/proxy/external-vod': {
+        target: 'https://slapibf.com',
+        changeOrigin: true,
+        rewrite: (path) => path.replace(/^\/api\/proxy\/external-vod/, '/api.php/provide/vod'),
+        configure: (proxy, _options) => {
+          proxy.on('error', (err, _req, _res) => {
+            console.log('proxy error', err);
+          });
+          proxy.on('proxyReq', (proxyReq, req, _res) => {
+            console.log('Sending Request to the Target:', req.method, req.url);
+          });
+          proxy.on('proxyRes', (proxyRes, req, _res) => {
+            console.log('Received Response from the Target:', proxyRes.statusCode, req.url);
+          });
+        },
+      },
+    },
   },
   build: {
     rollupOptions: {

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff