This commit is contained in:
2025-04-19 15:05:02 +08:00
parent 87531b92c5
commit aed20a0c79
13 changed files with 1480 additions and 257 deletions

View File

@@ -4,11 +4,11 @@
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<title>NBA在线</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script type="text/javascript" src="//js.users.51.la/21957239.js"></script>
<script type="text/javascript" src="https://js.users.51.la/21957239.js"></script>
</body>
</html>

View File

@@ -15,7 +15,9 @@
"element-plus": "^2.9.7",
"flv.js": "^1.6.2",
"hls.js": "^1.6.2",
"pinia": "^3.0.2",
"router": "^2.2.0",
"video.js": "^8.22.0",
"vue": "^3.5.13",
"vue-router": "4"
},

201
pnpm-lock.yaml generated
View File

@@ -26,9 +26,15 @@ importers:
hls.js:
specifier: ^1.6.2
version: 1.6.2
pinia:
specifier: ^3.0.2
version: 3.0.2(vue@3.5.13)
router:
specifier: ^2.2.0
version: 2.2.0
video.js:
specifier: ^8.22.0
version: 8.22.0
vue:
specifier: ^3.5.13
version: 3.5.13
@@ -182,6 +188,10 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/runtime@7.27.0':
resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==}
engines: {node: '>=6.9.0'}
'@babel/template@7.27.0':
resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==}
engines: {node: '>=6.9.0'}
@@ -528,6 +538,19 @@ packages:
'@types/web-bluetooth@0.0.16':
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
'@videojs/http-streaming@3.17.0':
resolution: {integrity: sha512-Ch1P3tvvIEezeZXyK11UfWgp4cWKX4vIhZ30baN/lRinqdbakZ5hiAI3pGjRy3d+q/Epyc8Csz5xMdKNNGYpcw==}
engines: {node: '>=8', npm: '>=5'}
peerDependencies:
video.js: ^8.19.0
'@videojs/vhs-utils@4.1.1':
resolution: {integrity: sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==}
engines: {node: '>=8', npm: '>=5'}
'@videojs/xhr@2.7.0':
resolution: {integrity: sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==}
'@vitejs/plugin-vue@5.2.3':
resolution: {integrity: sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -566,6 +589,9 @@ packages:
'@vue/devtools-api@6.6.4':
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
'@vue/devtools-api@7.7.5':
resolution: {integrity: sha512-HYV3tJGARROq5nlVMJh5KKHk7GU8Au3IrrmNNqr978m0edxgpHgYPDoNUGrvEgIbObz09SQezFR3A1EVmB5WZg==}
'@vue/devtools-core@7.7.5':
resolution: {integrity: sha512-ElKr0NDor57gVaT+gMQ8kcVP4uFGqHcxuuQndW/rPwh6aHWvEcUL3sxL8cEk+e1Rdt28kS88erpsiIMO6hEENQ==}
peerDependencies:
@@ -603,6 +629,13 @@ packages:
'@vueuse/shared@9.13.0':
resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
'@xmldom/xmldom@0.8.10':
resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==}
engines: {node: '>=10.0.0'}
aes-decrypter@4.0.2:
resolution: {integrity: sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==}
async-validator@4.2.5:
resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
@@ -693,6 +726,9 @@ packages:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
dom-walk@0.1.2:
resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==}
dplayer@1.27.1:
resolution: {integrity: sha512-2laBMXs5V1B9zPwJ7eAIw/OBo+Xjvy03i4GHTk3Cg+IWbrq8rKMFO0fFr6ClAYotYOCcFGOvaJDkOZcgKllsCA==}
@@ -809,6 +845,9 @@ packages:
resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==}
engines: {node: '>=18'}
global@4.4.0:
resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==}
globals@11.12.0:
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
engines: {node: '>=4'}
@@ -854,6 +893,9 @@ packages:
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
hasBin: true
is-function@1.0.2:
resolution: {integrity: sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==}
is-inside-container@1.0.0:
resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
engines: {node: '>=14.16'}
@@ -920,6 +962,9 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
m3u8-parser@7.2.0:
resolution: {integrity: sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==}
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
@@ -938,9 +983,16 @@ packages:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
min-document@2.19.0:
resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==}
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
mpd-parser@1.3.1:
resolution: {integrity: sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw==}
hasBin: true
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
@@ -948,6 +1000,11 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
mux.js@7.1.0:
resolution: {integrity: sha512-NTxawK/BBELJrYsZThEulyUMDVlLizKdxyAsMuzoCD1eFj97BVaA8D/CvKsKu6FOLYkFojN5CbM9h++ZTZtknA==}
engines: {node: '>=8', npm: '>=5'}
hasBin: true
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -1005,6 +1062,19 @@ packages:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
pinia@3.0.2:
resolution: {integrity: sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==}
peerDependencies:
typescript: '>=4.4.4'
vue: ^2.7.0 || ^3.5.11
peerDependenciesMeta:
typescript:
optional: true
pkcs7@1.0.4:
resolution: {integrity: sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==}
hasBin: true
postcss@8.5.3:
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
engines: {node: ^10 || ^12 || >=14}
@@ -1013,12 +1083,19 @@ packages:
resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==}
engines: {node: '>=18'}
process@0.11.10:
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
engines: {node: '>= 0.6.0'}
promise-polyfill@8.3.0:
resolution: {integrity: sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
regenerator-runtime@0.14.1:
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
rfdc@1.4.1:
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
@@ -1239,6 +1316,21 @@ packages:
varint@6.0.0:
resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==}
video.js@8.22.0:
resolution: {integrity: sha512-xge2kpjsvC0zgFJ1cqt+wTqsi21+huFswlonPFh7qiplypsb4FN/D2Rz6bWdG/S9eQaPHfWHsarmJL/7D3DHoA==}
videojs-contrib-quality-levels@4.1.0:
resolution: {integrity: sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==}
engines: {node: '>=16', npm: '>=8'}
peerDependencies:
video.js: ^8
videojs-font@4.2.0:
resolution: {integrity: sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ==}
videojs-vtt.js@0.15.5:
resolution: {integrity: sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==}
vite-hot-client@2.0.4:
resolution: {integrity: sha512-W9LOGAyGMrbGArYJN4LBCdOC5+Zwh7dHvOHC0KmGKkJhsOzaKbpo/jEjpPKVHIW0/jBWj8RZG0NUxfgA8BxgAg==}
peerDependencies:
@@ -1519,6 +1611,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/runtime@7.27.0':
dependencies:
regenerator-runtime: 0.14.1
'@babel/template@7.27.0':
dependencies:
'@babel/code-frame': 7.26.2
@@ -1739,6 +1835,28 @@ snapshots:
'@types/web-bluetooth@0.0.16': {}
'@videojs/http-streaming@3.17.0(video.js@8.22.0)':
dependencies:
'@babel/runtime': 7.27.0
'@videojs/vhs-utils': 4.1.1
aes-decrypter: 4.0.2
global: 4.4.0
m3u8-parser: 7.2.0
mpd-parser: 1.3.1
mux.js: 7.1.0
video.js: 8.22.0
'@videojs/vhs-utils@4.1.1':
dependencies:
'@babel/runtime': 7.27.0
global: 4.4.0
'@videojs/xhr@2.7.0':
dependencies:
'@babel/runtime': 7.27.0
global: 4.4.0
is-function: 1.0.2
'@vitejs/plugin-vue@5.2.3(vite@6.3.0(sass-embedded@1.86.3))(vue@3.5.13)':
dependencies:
vite: 6.3.0(sass-embedded@1.86.3)
@@ -1805,6 +1923,10 @@ snapshots:
'@vue/devtools-api@6.6.4': {}
'@vue/devtools-api@7.7.5':
dependencies:
'@vue/devtools-kit': 7.7.5
'@vue/devtools-core@7.7.5(vite@6.3.0(sass-embedded@1.86.3))(vue@3.5.13)':
dependencies:
'@vue/devtools-kit': 7.7.5
@@ -1874,6 +1996,15 @@ snapshots:
- '@vue/composition-api'
- vue
'@xmldom/xmldom@0.8.10': {}
aes-decrypter@4.0.2:
dependencies:
'@babel/runtime': 7.27.0
'@videojs/vhs-utils': 4.1.1
global: 4.4.0
pkcs7: 1.0.4
async-validator@4.2.5: {}
asynckit@0.4.0: {}
@@ -1957,6 +2088,8 @@ snapshots:
depd@2.0.0: {}
dom-walk@0.1.2: {}
dplayer@1.27.1:
dependencies:
axios: 1.2.3
@@ -2122,6 +2255,11 @@ snapshots:
'@sec-ant/readable-stream': 0.4.1
is-stream: 4.0.1
global@4.4.0:
dependencies:
min-document: 2.19.0
process: 0.11.10
globals@11.12.0: {}
gopd@1.2.0: {}
@@ -2150,6 +2288,8 @@ snapshots:
is-docker@3.0.0: {}
is-function@1.0.2: {}
is-inside-container@1.0.0:
dependencies:
is-docker: 3.0.0
@@ -2198,6 +2338,12 @@ snapshots:
dependencies:
yallist: 3.1.1
m3u8-parser@7.2.0:
dependencies:
'@babel/runtime': 7.27.0
'@videojs/vhs-utils': 4.1.1
global: 4.4.0
magic-string@0.30.17:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
@@ -2212,12 +2358,28 @@ snapshots:
dependencies:
mime-db: 1.52.0
min-document@2.19.0:
dependencies:
dom-walk: 0.1.2
mitt@3.0.1: {}
mpd-parser@1.3.1:
dependencies:
'@babel/runtime': 7.27.0
'@videojs/vhs-utils': 4.1.1
'@xmldom/xmldom': 0.8.10
global: 4.4.0
mrmime@2.0.1: {}
ms@2.1.3: {}
mux.js@7.1.0:
dependencies:
'@babel/runtime': 7.27.0
global: 4.4.0
nanoid@3.3.11: {}
nanoid@5.1.5: {}
@@ -2256,6 +2418,15 @@ snapshots:
picomatch@4.0.2: {}
pinia@3.0.2(vue@3.5.13):
dependencies:
'@vue/devtools-api': 7.7.5
vue: 3.5.13
pkcs7@1.0.4:
dependencies:
'@babel/runtime': 7.27.0
postcss@8.5.3:
dependencies:
nanoid: 3.3.11
@@ -2266,10 +2437,14 @@ snapshots:
dependencies:
parse-ms: 4.0.0
process@0.11.10: {}
promise-polyfill@8.3.0: {}
proxy-from-env@1.1.0: {}
regenerator-runtime@0.14.1: {}
rfdc@1.4.1: {}
rollup@4.40.0:
@@ -2463,6 +2638,32 @@ snapshots:
varint@6.0.0: {}
video.js@8.22.0:
dependencies:
'@babel/runtime': 7.27.0
'@videojs/http-streaming': 3.17.0(video.js@8.22.0)
'@videojs/vhs-utils': 4.1.1
'@videojs/xhr': 2.7.0
aes-decrypter: 4.0.2
global: 4.4.0
m3u8-parser: 7.2.0
mpd-parser: 1.3.1
mux.js: 7.1.0
videojs-contrib-quality-levels: 4.1.0(video.js@8.22.0)
videojs-font: 4.2.0
videojs-vtt.js: 0.15.5
videojs-contrib-quality-levels@4.1.0(video.js@8.22.0):
dependencies:
global: 4.4.0
video.js: 8.22.0
videojs-font@4.2.0: {}
videojs-vtt.js@0.15.5:
dependencies:
global: 4.4.0
vite-hot-client@2.0.4(vite@6.3.0(sass-embedded@1.86.3)):
dependencies:
vite: 6.3.0(sass-embedded@1.86.3)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -1,7 +1,8 @@
import request from "./request";
import axios from 'axios'
const nbaapi = axios.create({
baseURL: 'http://localhost:9005/api',
baseURL: 'http://api.new9.me/api',
// baseURL: 'http://110.42.255.182:8080',
timeout: 2000,
})
@@ -12,29 +13,72 @@ const urls = async () => {
method: 'get',
})
.then((response) => {
// console.log(response.data); // 可选:调试用
return response.data; // 返回数据
return response.data;
})
.catch((error) => {
console.error('获取直播URL失败:', error);
throw error; // 可以选择抛出错误或返回默认值,比如 return []
throw error;
});
};
const games = () => {
nbaapi({
const games = async () => {
return await nbaapi({
url: '/games',
method: 'get',
}).then((response) => {
console.log(response.data);
})
}
.then((response) => {
// console.log(response.data); // 调试用
return response.data; // 确保返回数据
})
.catch((error) => {
console.error('获取赛事数据失败:', error);
throw error; // 或者返回空数组 return []
});
};
const go = async (pwd) => {
return await nbaapi({
url: '/go',
method: 'get',
params: {
// 这里可以添加请求参数
pwd: pwd,
},
})
.then((response) => {
// console.log(response.data); // 调试用
return response.data; // 确保返回数据
})
.catch((error) => {
console.error('获取赛事数据失败:', error);
throw error; // 或者返回空数组 return []
});
};
const schedule = (params) => {
return request({
url: '/game/schedule',
method: 'get',
params: params,
});
}
};
export {schedule,games,urls};
const addUrls = async (gameId, urls) => {
return await nbaapi({
url: '/addUrls',
method: 'post',
data: {
gameId: gameId,
urls: urls
}
})
.then((response) => {
return response.data;
})
.catch((error) => {
console.error('添加直播URL失败:', error);
throw error;
});
};
export { schedule, games, urls, go,addUrls };

View File

@@ -1,196 +1,296 @@
<template>
<div class="video-container">
<div
v-for="(video, index) in videoList"
:key="index"
class="video-card"
@click="handleCardClick(index)"
<div class="live-stream-container">
<!-- 比赛信息 -->
<div class="game-header" v-if="gameData">
<div class="team-info away-team">
<img :src="gameData.awayTeam.logo" :alt="gameData.awayTeam.name" />
<div class="team-details">
<h3>{{ gameData.awayTeam.city }}</h3>
<p>{{ gameData.awayTeam.name }}</p>
<span>{{ gameData.awayTeam.record }}</span>
</div>
</div>
<div class="vs-circle">VS</div>
<div class="team-info home-team">
<img :src="gameData.homeTeam.logo" :alt="gameData.homeTeam.name" />
<div class="team-details">
<h3>{{ gameData.homeTeam.city }}</h3>
<p>{{ gameData.homeTeam.name }}</p>
<span>{{ gameData.homeTeam.record }}</span>
</div>
</div>
</div>
<!-- 播放器 -->
<div id="dplayer-live" class="dplayer-container"></div>
<!-- 直播源列表始终显示 -->
<div class="stream-switcher" v-if="allStreams.length > 0">
<!-- <h3>直播源</h3> -->
<div class="stream-buttons">
<button
v-for="stream in allStreams"
:key="stream.type"
@click="switchStream(stream)"
:class="{ active: currentStream?.type === stream.type }"
>
<div class="video-wrapper">
<div :id="'dplayer-' + index" class="dplayer-container"></div>
</div>
<div class="video-info">
<h3 class="video-title">{{ video.title }}</h3>
<p class="video-desc">{{ video.description }}</p>
{{ getStreamName(stream.type) }}
</button>
</div>
</div>
<!-- 返回按钮 -->
<button class="back-button" @click="goBack">
返回赛程
</button>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useGameStore } from '@/stores/game'
import DPlayer from 'dplayer'
import Hls from 'hls.js'
import Flv from 'flv.js'
// 将 flv.js 注册为全局变量
// 注册全局变量
window.flvjs = Flv
window.Hls = Hls
// 视频列表数据
const videoList = ref([
{
title: '示例视频1',
description: '这是一个示例视频描述',
// url: './../../public/videos/7.mp4',
url: 'https://feijing-xzbonlinepull.bszb.me/live/202_3520771_1.m3u8?txSecret=92b9a5df1d71b2a1dbf4cb41e7c7b507&txTime=68011a9d',
// pic: 'https://example.com/poster1.jpg'
},
// {
// title: '示例视频2',
// description: '这是另一个示例视频描述',
// url: 'https://example.com/video2.mp4',
// pic: 'https://example.com/poster2.jpg'
// },
// 可以添加更多视频
])
const dpInstances = ref([])
const router = useRouter()
const gameStore = useGameStore()
const dpInstance = ref(null)
// 从store获取数据
const gameData = computed(() => gameStore.currentGame)
const allStreams = computed(() => gameStore.allStreams)
const currentStream = computed({
get: () => gameStore.currentStream,
set: (val) => gameStore.currentStream = val
})
// 初始化播放器
const initDPlayers = () => {
videoList.value.forEach((video, index) => {
const dp = new DPlayer({
container: document.getElementById(`dplayer-${index}`),
// live: true,
screenshot: true,
const initPlayer = () => {
if (dpInstance.value) {
dpInstance.value.destroy();
}
dpInstance.value = new DPlayer({
container: document.getElementById('dplayer-live'),
live: true,
autoplay: true,
theme: '#b7daff',
loop: false,
lang: 'zh-cn',
hotkey: true,
preload: 'auto',
volume: 0.6,
video: {
url: video.url,
pic: video.pic,
thumbnails: video.pic,
type: 'auto',
},
pluginOptions:{
hls: {
// 这里可以添加 HLS.js 的配置选项
debug: true,
enableWorker: true,
manifestLoadingTimeOut: 10000,
levelLoadingTimeOut: 10000,
},
flv: {
// 这里可以添加 FLV.js 的配置选项
enableWorker: true,
enableStashBuffer: true,
stashInitialSize: 128,
url: currentStream.value?.url || '',
type: 'auto'
}
}
})
console.log(dp.plugins.flv); // flv 实例
// 监听播放器的事件
dpInstances.value.push(dp)
});
})
// 强制设置视频尺寸
setTimeout(() => {
const container = document.getElementById('dplayer-live');
const video = container?.querySelector('video');
if (video) {
video.style.cssText = `
width: 100% !important;
height: 100% !important;
object-fit: contain !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
`;
// console.log('视频实际尺寸:', video.videoWidth, 'x', video.videoHeight);
}
}, 500);
};
// 切换直播源
const switchStream = (stream) => {
currentStream.value = stream
initPlayer()
}
// 处理卡片点击事件
const handleCardClick = (index) => {
// 暂停所有其他播放器
dpInstances.value.forEach((dp, i) => {
if (i !== index && !dp.video.paused) {
dp.pause()
// 获取直播源名称
const getStreamName = (type) => {
const names = {
tx: '企鹅体育',
wl: '纬来体育',
mg: '咪咕体育',
nba: '高清原声',
zb: '高清直播'
}
})
return names[type] || type
}
// 返回赛程页
const goBack = () => {
gameStore.clearGameData()
router.go(-1)
}
onMounted(() => {
initDPlayers()
// 默认选择第一个直播源
if (allStreams.value.length > 0 && !currentStream.value) {
currentStream.value = allStreams.value[0]
}
initPlayer()
})
onBeforeUnmount(() => {
// 销毁所有播放器实例
dpInstances.value.forEach(dp => {
dp.destroy()
})
if (dpInstance.value) {
dpInstance.value.destroy()
}
})
</script>
<style lang="scss" scoped>
.video-container {
.live-stream-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
min-height: 100vh;
}
.game-header {
display: flex;
flex-wrap: wrap;
padding: 40px 0;
justify-content: center;
// align-items: center;
.video-card {
flex: 1 1 calc(33.333% - 20px);
min-width: 768px;
max-width: 70%;
background: #d6f3f0;
align-items: center;
gap: 40px;
margin-bottom: 20px;
background: white;
padding: 20px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
cursor: pointer;
&:hover {
// transform: translateY(-5px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.video-wrapper {
position: relative;
padding-top: 56.25%; /* 16:9 宽高比 */
overflow: hidden;
.team-info {
display: flex;
align-items: center;
gap: 15px;
.dplayer-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
img {
width: 80px;
height: 80px;
object-fit: contain;
}
.video-info {
padding: 15px;
.team-details {
text-align: center;
.video-title {
margin: 0 0 8px 0;
font-size: 1.1rem;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.video-desc {
h3 {
margin: 0;
font-size: 1.2rem;
color: #333;
}
p {
margin: 5px 0;
font-size: 1.1rem;
font-weight: bold;
}
span {
font-size: 0.9rem;
color: #666;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
}
}
/* 移动端适配 */
.vs-circle {
width: 50px;
height: 50px;
background: #e74c3c;
color: white;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.2rem;
font-weight: bold;
}
.dplayer-container {
width: 100%;
aspect-ratio: 16/9;
position: relative;
background: #000;
::v-deep {
.dplayer-video {
width: 100% !important;
height: 100% !important;
object-fit: contain !important;
}
}
}
.stream-switcher {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-top: 20px;
}
.stream-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
button {
padding: 8px 16px;
background: #f0f0f0;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #e0e0e0;
}
&.active {
background: #3498db;
color: white;
}
}
}
.back-button {
display: block;
width: 200px;
margin: 30px auto 0;
padding: 12px 24px;
background: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
text-align: center;
&:hover {
background: #2980b9;
}
}
@media (max-width: 768px) {
.video-container {
.video-card {
flex: 1 1 calc(50% - 15px);
min-width: 100%;
}
}
.game-header {
flex-direction: column;
gap: 20px;
}
@media (max-width: 480px) {
.video-container {
.video-card {
flex: 1 1 100%;
min-width: 100%;
.vs-circle {
margin: 10px 0;
}
.back-button {
width: 100%;
}
}
</style>

View File

@@ -2,12 +2,12 @@
<div class="nba-schedule-container">
<!-- 赞助商信息 -->
<div v-if="scheduleData?.data?.sponsor" class="sponsor-banner">
<span>所有内容均来源互联网如有侵权联系邮箱xdd9@vip.qq.com</span>
<img
<span>所有内容均来源互联网有问题请联系邮箱xdd9@vip.qq.com</span>
<!-- <img
:src="scheduleData.data.sponsor.logo"
:alt="scheduleData.data.sponsor.name"
class="sponsor-logo"
/>
/> -->
</div>
<!-- 赛程日期导航 -->
@@ -49,7 +49,10 @@
<!-- 客队信息 -->
<div
class="team away-team"
:class="{ 'tbd-team': !game.teamValid }"
:class="{
'tbd-team': !game.teamValid,
winner: isWinner(game, 'away'), // 添加判断是否为胜者
}"
>
<img
:src="game.awayTeamLogoDark"
@@ -91,7 +94,10 @@
<!-- 主队信息 -->
<div
class="team home-team"
:class="{ 'tbd-team': !game.teamValid }"
:class="{
'tbd-team': !game.teamValid,
winner: isWinner(game, 'home'), // 添加判断是否为胜者
}"
>
<img
:src="game.homeTeamLogoDark"
@@ -115,25 +121,27 @@
</div>
</div>
<!-- 直播间按钮区域仅当天进行中的比赛显示 -->
<div class="live-buttons" v-if="shouldShowLiveArea(game)">
<template v-if="game.status === 2 && hasLiveStreams(game.gameId)">
<div class="live-buttons">
<!-- 只有当比赛未结束且是当天比赛时才显示直播区域 -->
<template v-if="game.status !== 3 && shouldShowLiveArea(game)">
<template v-if="hasLiveStreams(game.gameId)">
<!-- 直播按钮 -->
<button
v-for="stream in getLiveStreams(game.gameId)"
:key="stream.type"
@click="goToLive(game, stream)"
class="live-btn"
:class="{
primary: stream.type === 'qq',
secondary: stream.type !== 'qq',
}"
@click="goToLive(stream.url)"
>
<span class="btn-icon">📺</span>
<span>{{ getStreamName(stream.type) }}</span>
{{ getStreamName(stream.type) }}
</button>
</template>
<div v-else-if="game.status === 1" class="no-live">未开始</div>
<div v-else class="no-live">无直播信号</div>
</template>
<div v-else-if="game.status === 3" class="no-live">
比赛已结束
</div>
<div v-else class="no-live">未开始</div>
</div>
<!-- 比赛场地和赛季信息 -->
@@ -162,7 +170,8 @@ import { computed, ref } from "vue";
import { useRouter } from "vue-router";
import { urls } from "@/api/nba";
import { onMounted } from "vue";
import { useGameStore } from "@/stores/game";
const gameStore = useGameStore();
const router = useRouter();
const urlsData = ref([]);
@@ -170,22 +179,21 @@ const shouldShowLiveArea = (game) => {
// 1. 已结束的比赛不显示
if (game.status === 3) return false;
const gameDate = new Date(game.dateTimeUtc);
// 2. 获取今天的日期(北京时间)
const today = new Date();
const todayStr = `${today.getFullYear()}-${(today.getMonth() + 1)
.toString()
.padStart(2, "0")}-${today.getDate().toString().padStart(2, "0")}`;
// 2. 只显示当天及未来的比赛
// 清除时间部分,只比较日期
today.setHours(0, 0, 0, 0);
gameDate.setHours(0, 0, 0, 0);
return gameDate >= today;
// 3. 直接比较 startDate已经是北京时间
return game.startDate === todayStr;
};
onMounted(async () => {
try {
const response = await urls();
urlsData.value = response || [];
// console.log("获取的直播URL:", urlsData.value);
// console.log("获取的直播URL数据:", urlsData.value); // 检查数据是否正确
} catch (err) {
console.error("获取直播URL失败:", err);
urlsData.value = [];
@@ -205,25 +213,31 @@ const isLiveGame = (game) => {
const hasLiveStreams = (gameId) => {
if (!urlsData.value || !gameId) return false;
// 查找匹配的gameId
const gameStreams = urlsData.value.find((item) => item[gameId]);
return !!gameStreams;
// 遍历所有直播流数据
for (const streamGroup of urlsData.value) {
if (streamGroup[gameId]) {
return true;
}
}
return false;
};
// 获取比赛的直播流
const getLiveStreams = (gameId) => {
if (!urlsData.value || !gameId) return [];
const gameStreams = urlsData.value.find((item) => item[gameId]);
return gameStreams ? gameStreams[gameId] : [];
const id = String(gameId); // 转为字符串
for (const streamGroup of urlsData.value) {
if (streamGroup[id]) return streamGroup[id];
}
return [];
};
// 获取流名称
const getStreamName = (type) => {
const names = {
tx: "TX直播",
wl: "纬来直播",
nba: "原声直播",
tx: "企鹅体育",
wl: "纬来体育",
nba: "高清原声",
mg: "咪咕体育",
zb: "高清直播",
// 可以添加更多类型
};
@@ -231,14 +245,39 @@ const getStreamName = (type) => {
};
// 跳转到直播页面
const goToLive = (url) => {
if (!url) return;
const goToLive = (game, stream) => {
// 准备比赛数据
const gameData = {
homeTeam: {
name: game.homeTeamName,
logo: game.homeTeamLogoDark,
city: game.homeTeamCity,
record: `${game.homeTeamWins}胜-${game.homeTeamLosses}`,
},
awayTeam: {
name: game.awayTeamName,
logo: game.awayTeamLogoDark,
city: game.awayTeamCity,
record: `${game.awayTeamWins}胜-${game.awayTeamLosses}`,
},
gameInfo: {
arena: game.arenaName,
season: game.seasonName,
},
};
// 存储到Pinia
gameStore.setCurrentGame({
gameData,
currentStream: stream,
allStreams: getLiveStreams(game.gameId),
});
// 导航到播放页
router.push({
name: "Play",
query: {
url: url,
// 其他参数...
params: {
gameId: game.gameId,
},
});
};
@@ -324,6 +363,19 @@ const changeDate = (direction) => {
emit("dateChange", date);
}
};
// 判断某支球队是否是胜者
const isWinner = (game, teamType) => {
// 如果比赛未结束,没有胜者
if (game.status !== 3) return false;
// 比较比分
if (teamType === 'away') {
return game.awayTeamScore > game.homeTeamScore;
} else {
return game.homeTeamScore > game.awayTeamScore;
}
};
</script>
<style scoped>
@@ -520,8 +572,8 @@ const changeDate = (direction) => {
}
.game-not-started {
color: #6c757d;
font-size: 16px;
color: #5a7cec;
font-size: 18px;
}
.game-in-progress {
@@ -693,4 +745,20 @@ const changeDate = (direction) => {
border-radius: 8px;
margin: 20px 0;
}
/* 胜者背景色 */
.team.winner {
background-color: rgba(76, 175, 80, 0.1); /* 浅绿色背景 */
border-left: 3px solid #74fd79; /* 左侧绿色边框 */
}
/* 如果希望更明显的效果,可以调整样式 */
.team.winner .team-name {
font-weight: bold;
color: #2E7D32; /* 深绿色文字 */
}
.team.winner .team-score {
font-weight: bold;
color: #2E7D32; /* 深绿色比分 */
}
</style>

View File

@@ -1,9 +1,12 @@
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import { createPinia } from 'pinia'
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.use(router)
app.use(ElementPlus)
app.use(createPinia())
app.mount('#app')

View File

@@ -2,6 +2,10 @@
import { createRouter, createWebHistory } from 'vue-router'
import IndexVue from '@/views/Index.vue'
import PlayVue from '@/views/Play.vue'
import AdminVue from '@/views/Admin.vue'
import { useGameStore } from '@/stores/game'
import TestVue from '@/views/Test.vue'
const routes = [
{
path: '/',
@@ -10,10 +14,25 @@ const routes = [
props: route => ({ query: route.query })
},
{
path: '/play',
path: '/play/:gameId',
name: 'Play',
component: PlayVue,
props: route => ({ query: route.query })
component: () => import('@/views/Play.vue'),
props: true // 启用props接收路由参数
},
{
path: '/lives',
name: 'Admin',
component: AdminVue,
},
{
path: '/test',
name: 'Test',
component: TestVue,
},
// 添加通配符路由,捕获所有未匹配的路径
{
path: '/:pathMatch(.*)*',
redirect: '/'
}
]
@@ -22,4 +41,19 @@ const router = createRouter({
routes
})
// 添加全局路由守卫
router.beforeEach((to, from) => {
const gameStore = useGameStore()
// 离开播放页时清理数据
if (from.name === 'Play' && to.name !== 'Play') {
gameStore.clearGameData()
}
// 进入播放页时检查数据
if (to.name === 'Play' && !gameStore.currentGame) {
return '/' // 无数据则重定向到首页
}
})
export default router

39
src/stores/game.js Normal file
View File

@@ -0,0 +1,39 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useGameStore = defineStore('game', () => {
// 状态
const _currentGame = ref(null)
const _currentStream = ref(null)
const _allStreams = ref([])
// 计算属性(保持响应式)
const currentGame = computed(() => _currentGame.value)
const allStreams = computed(() => _allStreams.value)
const currentStream = computed({
get: () => _currentStream.value,
set: (val) => _currentStream.value = val // 确保可写
})
// 设置当前比赛
const setCurrentGame = (data) => {
_currentGame.value = data.gameData
_currentStream.value = data.currentStream
_allStreams.value = data.allStreams
}
// 清除数据
const clearGameData = () => {
_currentGame.value = null
_currentStream.value = null
_allStreams.value = []
}
return {
currentGame,
currentStream,
allStreams,
setCurrentGame,
clearGameData
}
})

579
src/views/Admin.vue Normal file
View File

@@ -0,0 +1,579 @@
<template>
<div class="games-container">
<h2>NBA赛事列表</h2>
<div class="game-list">
<div v-for="game in gamesData" :key="game.id" class="game-item">
<div class="game-info">
<span class="game-date">{{ formatDate(game.date) }}</span>
<span class="game-id">ID: {{ game.gameId }}</span>
<div class="teams">
<div class="team">
<img
:src="game.homeTeamLogoDark"
:alt="game.homeTeamName"
class="team-logo"
/>
<span class="team-name">{{ game.homeTeamName }}</span>
</div>
<span class="vs">VS</span>
<div class="team">
<img
:src="game.awayTeamLogoDark"
:alt="game.awayTeamName"
class="team-logo"
/>
<span class="team-name">{{ game.awayTeamName }}</span>
</div>
</div>
<span class="start-time">{{ formatTime(game.startTime) }}</span>
<!-- 显示直播URL -->
<div
v-if="getGameUrls(game.gameId).length > 0"
class="urls-container"
>
<h4>直播链接:</h4>
<div
v-for="(url, index) in getGameUrls(game.gameId)"
:key="index"
class="url-item"
>
<span class="url-type">{{ formatUrlType(url.type) }}:</span>
<a :href="url.url" target="_blank" class="url-link">{{
url.url
}}</a>
</div>
</div>
<div v-else class="no-urls">暂无直播链接</div>
</div>
<button class="add-url-btn" @click="openAddUrlDialog(game)">
添加直播
</button>
</div>
</div>
<!-- 添加直播URL的对话框 -->
<!-- 添加直播URL的对话框 -->
<div v-if="showDialog" class="dialog-overlay">
<div class="dialog-content">
<h3>
{{ selectedGame.homeTeamName }} VS
{{ selectedGame.awayTeamName }} 添加直播URL
</h3>
<div class="url-inputs">
<div v-for="(url, index) in newUrls" :key="index" class="url-item">
<div class="form-group">
<label>直播类型 {{ index + 1 }}:</label>
<select v-model="url.type" class="url-type-select">
<option value="tx">腾讯体育</option>
<option value="mg">咪咕视频</option>
<option value="wl">纬来体育</option>
<option value="nba">NBA原声</option>
<option value="zb">其他平台</option>
</select>
</div>
<div class="form-group">
<label>直播地址 {{ index + 1 }}:</label>
<input
v-model="url.url"
type="text"
placeholder="请输入完整的直播URL"
class="url-input"
/>
</div>
<button
v-if="newUrls.length > 1"
class="remove-btn"
@click="removeUrl(index)"
>
<i class="el-icon-remove"></i> 删除
</button>
</div>
</div>
<button class="add-more-btn" @click="addMoreUrl">
<i class="el-icon-circle-plus"></i> 添加更多直播源
</button>
<div class="dialog-footer">
<button class="cancel-btn" @click="closeDialog">取消</button>
<button
class="confirm-btn"
@click="submitUrls"
:disabled="isSubmitting"
>
<span v-if="!isSubmitting">确认添加</span>
<span v-else>正在提交...</span>
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeMount } from "vue";
import { ElMessage, ElLoading } from "element-plus";
import { games, urls, addUrls as apiAddUrls,go } from "@/api/nba";
// 组件状态
const gamesData = ref([]);
const urlData = ref([]);
const showDialog = ref(false);
const selectedGame = ref(null);
const newUrls = ref([{ type: "tx", url: "" }]);
const isSubmitting = ref(false);
// 获取比赛和URL数据
onMounted(async () => {
try {
const [urlsRes, gamesRes] = await Promise.all([urls(), games()]);
urlData.value = urlsRes || [];
gamesData.value = gamesRes || [];
} catch (err) {
console.error("获取数据失败:", err);
ElMessage.error("获取比赛数据失败,请刷新重试");
}
});
// 提交直播链接
const submitUrls = async () => {
// 验证URL格式
const validUrls = newUrls.value
.filter((item) => item.url.trim() !== "")
.map((item) => ({
type: item.type,
url: item.url.trim(),
}));
if (validUrls.length === 0) {
ElMessage.warning("请至少输入一个有效的直播URL");
return;
}
// 验证URL格式是否正确
for (const url of validUrls) {
if (!isValidUrl(url.url)) {
ElMessage.warning(`直播地址格式不正确: ${url.url}`);
return;
}
}
isSubmitting.value = true;
const loading = ElLoading.service({
lock: true,
text: "正在提交直播链接...",
background: "rgba(0, 0, 0, 0.7)",
});
try {
// 调用API添加URL
await apiAddUrls(selectedGame.value.gameId, validUrls);
// 更新本地数据
updateLocalUrls(selectedGame.value.gameId, validUrls);
ElMessage.success("直播链接添加成功!");
closeDialog();
} catch (error) {
console.error("添加直播URL失败:", error);
ElMessage.error(`添加失败: ${error.message || "服务器错误"}`);
} finally {
loading.close();
isSubmitting.value = false;
}
};
// 更新本地URL数据
const updateLocalUrls = (gameId, urlsToAdd) => {
const existingIndex = urlData.value.findIndex(
(item) => item.gameId === gameId
);
if (existingIndex >= 0) {
// 合并现有URL
urlData.value[existingIndex].urls = [
...urlData.value[existingIndex].urls,
...urlsToAdd,
];
} else {
// 添加新比赛URL
urlData.value.push({
gameId,
urls: urlsToAdd,
});
}
};
// URL验证函数
const isValidUrl = (url) => {
try {
new URL(url);
return true;
} catch {
return false;
}
};
onBeforeMount(async () => {
const storedPassword = localStorage.getItem("password");
if (storedPassword && storedPassword == "inspur123") {
console.log("=======进入后台======");
// return;
} else {
const password = prompt("请输入密码:");
try {
const response = await go(password);
if (response == true) {
// console.log("=======进入后台======");
localStorage.setItem("password", password);
} else {
// console.log("==========密码错误=========");
window.location.href = "/";
return;
}
} catch (err) {
// console.error("验证密码失败:", err);
window.location.href = "/";
}
}
});
onMounted(async () => {
try {
const [res_urls, response] = await Promise.all([urls(), games()]);
urlData.value = res_urls || [];
gamesData.value = response || [];
} catch (err) {
console.error("获取数据失败:", err);
gamesData.value = [];
urlData.value = [];
}
});
// 根据gameId获取对应的URLs
const getGameUrls = (gameId) => {
const gameUrls = urlData.value.find((item) => item[gameId]);
return gameUrls ? gameUrls[gameId] : [];
};
// 格式化URL类型显示
const formatUrlType = (type) => {
const typeMap = {
tx: "腾讯",
wl: "纬来",
mg: "咪咕",
nba: "原声",
zb: "其他",
qq: "腾讯",
wx: "微信",
};
return typeMap[type] || type;
};
const openAddUrlDialog = (game) => {
selectedGame.value = game;
newUrls.value = [{ type: "tx", url: "" }];
showDialog.value = true;
};
const closeDialog = () => {
showDialog.value = false;
};
const addMoreUrl = () => {
newUrls.value.push({ type: "tx", url: "" });
};
const removeUrl = (index) => {
newUrls.value.splice(index, 1);
};
const addUrls = async () => {
const validUrls = newUrls.value.filter((item) => item.url.trim() !== "");
if (validUrls.length === 0) {
alert("请至少输入一个有效的直播URL");
return;
}
const payload = {
gameId: selectedGame.value.gameId,
urls: validUrls,
};
try {
// console.log("提交的数据:", payload);
alert("直播URL添加成功!");
closeDialog();
} catch (error) {
console.error("添加直播URL失败:", error);
alert("添加失败,请重试");
}
};
const formatDate = (dateString) => {
const date = new Date(dateString);
return `${date.getMonth() + 1}${date.getDate()}`;
};
const formatTime = (timeString) => {
const time = new Date(timeString);
return `${time.getHours()}:${time.getMinutes().toString().padStart(2, "0")}`;
};
</script>
<style lang="scss" scoped>
/* 添加一些样式使URL显示更美观 */
.urls-container {
margin-top: 10px;
padding: 10px;
background-color: #f5f5f5;
border-radius: 4px;
}
.url-item {
margin-bottom: 5px;
display: flex;
align-items: center;
}
.url-type {
font-weight: bold;
margin-right: 8px;
min-width: 40px;
}
.url-link {
color: #0066cc;
text-decoration: none;
word-break: break-all;
}
.url-link:hover {
text-decoration: underline;
}
.no-urls {
margin-top: 10px;
color: #999;
font-style: italic;
}
.games-container {
padding: 20px;
max-width: 1000px;
margin: 0 auto;
}
h2 {
text-align: center;
margin-bottom: 20px;
color: #333;
}
.game-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.game-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background-color: #f5f5f5;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.game-info {
display: flex;
align-items: center;
gap: 20px;
}
.game-date,
.game-id,
.start-time {
font-size: 14px;
color: #666;
}
.teams {
display: flex;
align-items: center;
gap: 10px;
}
.team {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.team-logo {
width: 40px;
height: 40px;
object-fit: contain;
}
.team-name {
font-size: 14px;
font-weight: 500;
}
.vs {
font-weight: bold;
color: #e63946;
}
.add-url-btn {
padding: 8px 15px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: #45a049;
}
}
/* 对话框样式 */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.dialog-content {
background-color: white;
padding: 25px;
border-radius: 8px;
width: 90%;
max-width: 500px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.dialog-content h3 {
margin-top: 0;
color: #333;
text-align: center;
margin-bottom: 20px;
}
.url-inputs {
max-height: 400px;
overflow-y: auto;
margin-bottom: 15px;
}
.url-item {
background-color: #f9f9f9;
padding: 15px;
border-radius: 6px;
margin-bottom: 15px;
position: relative;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.remove-btn {
position: absolute;
top: 10px;
right: 10px;
background-color: #ff4444;
color: white;
border: none;
border-radius: 4px;
padding: 2px 8px;
cursor: pointer;
font-size: 12px;
&:hover {
background-color: #cc0000;
}
}
.add-more-btn {
width: 100%;
padding: 8px;
background-color: #f5f5f5;
border: 1px dashed #ccc;
border-radius: 4px;
cursor: pointer;
margin-bottom: 15px;
&:hover {
background-color: #e0e0e0;
}
}
.dialog-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.cancel-btn,
.confirm-btn {
padding: 8px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
.cancel-btn {
background-color: #f5f5f5;
color: #333;
&:hover {
background-color: #e0e0e0;
}
}
.confirm-btn {
background-color: #2196f3;
color: white;
&:hover {
background-color: #0b7dda;
}
}
</style>

View File

@@ -1,26 +1,18 @@
<template>
<!-- <LiveStream
:liveUrl="liveUrl"
:streamType="streamType"
:gameInfo="gameInfo"
@goBack="goBack"
/> -->
<LiveStream />
<LiveStream @goBack="goBack" />
</template>
<script setup>
import LiveStream from '@/components/LiveStream.vue';
// import { ref } from 'vue';
// import { useRoute, useRouter } from 'vue-router'
// const route = useRoute(); // 注意这里改成了 route 而不是 useRoute
// const liveUrl = ref('');
// liveUrl.value = route.query.url;
// console.log('play页面接收的直播地址:', liveUrl.value);
import { useRouter } from 'vue-router'
import LiveStream from '@/components/LiveStream.vue'
const router = useRouter()
const goBack = () => {
router.go(-1)
}
</script>
<style lang="scss" scoped>
/* 可以添加一些页面样式 */
</style>

161
src/views/Test.vue Normal file
View File

@@ -0,0 +1,161 @@
<template>
<div class="player-container">
<!-- 播放器容器 -->
<div id="dplayer" ref="dplayerRef"></div>
<!-- 浏览器不支持提示 -->
<div v-if="!isSupported" class="unsupported-tip">
当前浏览器不支持HLS直播流播放请使用Chrome/Firefox/Edge等现代浏览器
</div>
<!-- 移动端自动播放提示 -->
<div v-if="showPlayButton" class="play-button" @click="handleClickPlay">
<span class="icon"></span>
<span>点击播放</span>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import DPlayer from 'dplayer'
import Hls from 'hls.js'
// import 'dplayer/dist/DPlayer.min.css'
const dplayerRef = ref(null)
const dp = ref(null)
const isSupported = ref(true)
const showPlayButton = ref(false)
// 初始化播放器
const initPlayer = () => {
// 销毁旧实例
if (dp.value) {
dp.value.destroy()
}
const options = {
container: dplayerRef.value,
live: true,
autoplay: !isMobile(), // 非移动端自动播放
video: {
url: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8', // 替换为实际m3u8地址
type: 'customHls',
customType: {
customHls: (video, player) => {
const hls = new Hls({
enableWorker: true, // 启用HLS.js的Web Worker
maxBufferLength: 30, // 最大缓冲长度(秒)
maxMaxBufferLength: 600, // 最大缓冲限制
maxBufferSize: 60 * 1000 * 1000, // 最大缓冲大小(bytes)
maxBufferHole: 0.5 // 最大允许的缓冲缺口(秒)
})
hls.loadSource(video.src)
hls.attachMedia(video)
// 错误处理
hls.on(Hls.Events.ERROR, (event, data) => {
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
console.error('网络错误,尝试重新加载')
hls.startLoad()
break
case Hls.ErrorTypes.MEDIA_ERROR:
console.error('媒体错误,尝试恢复')
hls.recoverMediaError()
break
default:
initPlayer() // 其他错误重新初始化
}
}
})
player.on('destroy', () => hls.destroy())
}
}
}
}
// 如果是Safari且支持原生HLS
if (isSafari() && video.canPlayType('application/vnd.apple.mpegurl')) {
options.video.type = 'hls'
delete options.video.customType
}
dp.value = new DPlayer(options)
// 移动端需要用户交互后才能播放
if (isMobile()) {
showPlayButton.value = true
dp.value.pause()
}
}
// 检测移动端
const isMobile = () => {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
}
// 检测Safari浏览器
const isSafari = () => {
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
}
// 点击播放按钮
const handleClickPlay = () => {
dp.value.play()
showPlayButton.value = false
}
onMounted(() => {
// 检查浏览器支持情况
if (!Hls.isSupported() && !video.canPlayType('application/vnd.apple.mpegurl')) {
isSupported.value = false
return
}
initPlayer()
})
onBeforeUnmount(() => {
if (dp.value) {
dp.value.destroy()
}
})
</script>
<style lang="scss" scoped>
.player-container {
position: relative;
width: 100%;
max-width: 800px;
margin: 0 auto;
background-color: #000;
#dplayer {
width: 100%;
height: 0;
padding-bottom: 56.25%; /* 16:9 比例 */
position: relative;
overflow: hidden;
/* 确保 DPlayer 内部视频元素正确填充 */
& > div {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* 强制视频填充整个播放器 */
video {
width: 100%;
height: 100%;
object-fit: contain; /* 或使用 'cover' 填充整个容器(可能裁剪边缘) */
}
}
}
</style>