feat: 首次提交NBA前端项目
This commit is contained in:
33
SEO_NOTES.md
Normal file
33
SEO_NOTES.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
SEO 改进说明
|
||||||
|
|
||||||
|
已完成的改动:
|
||||||
|
- 在 `index.html` 中添加了更完整的 meta(robots、canonical 占位、Open Graph、Twitter Card、JSON-LD)。
|
||||||
|
- 在 `src/router/index.js` 中为路由添加了 `meta.title` 与 `meta.description`,并添加了 `afterEach` 钩子,用于在单页应用导航后更新 `document.title`、`meta description` 与 `canonical`。
|
||||||
|
- 在 `public/` 下添加了 `robots.txt` 与静态 `sitemap.xml`(请替换其中的 `REPLACE_WITH_YOUR_SITE_URL` 为真实域名)。
|
||||||
|
|
||||||
|
1. 已替换占位符
|
||||||
|
- 我已将所有 `REPLACE_WITH_YOUR_SITE_URL` 替换为你提供的域名 `http://nba.1024x.icu/`(包括 `index.html`、`public/robots.txt`、`public/sitemap.xml`)。请确认你已在该域名下部署网站,并把 `og-image.png` 上传至站点根目录(或修改 meta 指向的图片地址)。
|
||||||
|
2. 自动化 sitemap(推荐)
|
||||||
|
|
||||||
|
2. 自动化 sitemap(推荐)
|
||||||
|
- 如果你有大量带参数的播放页面(/play/:gameId),建议在构建或后台生成 sitemap 时把具体页面 URL 列入 sitemap(例如在 CI 中运行脚本生成 sitemap.xml)。
|
||||||
|
|
||||||
|
3. SSR / 预渲染
|
||||||
|
- 单页应用(SPA)依赖客户端渲染,部分搜索引擎和社交分享抓取可能受限。建议采用:
|
||||||
|
- 服务端渲染(Nuxt / Vite SSR)或
|
||||||
|
- 构建时预渲染(prerender-spa-plugin / vite-plugin-prerender)来生成静态 HTML,提升首屏可抓取性。
|
||||||
|
|
||||||
|
4. 提交站点地图与验证站点
|
||||||
|
- 在 Google Search Console、Bing Webmaster 提交 sitemap(/sitemap.xml)并验证站点所有权。
|
||||||
|
|
||||||
|
5. 其它优化点(可选)
|
||||||
|
- 添加 hreflang(多语言站点)。
|
||||||
|
- 为重要页面添加结构化的 Article/Event/Video Schema(JSON-LD)。
|
||||||
|
- 提高页面加载性能(Lighthouse 得分)以提升搜索排名。
|
||||||
|
|
||||||
|
如何回滚或微调
|
||||||
|
- 如果想回退 `index.html` 或路由的修改,请使用版本控制(git)回退这些文件。修改完成后,重新构建并部署。
|
||||||
|
|
||||||
|
如果你愿意,我可以:
|
||||||
|
- 替你把 `REPLACE_WITH_YOUR_SITE_URL` 批量替换为实际域名(请提供域名);
|
||||||
|
- 添加一个构建时脚本生成 sitemap(把你希望列出的动态页面列表发给我)。
|
||||||
77
index.html
77
index.html
@@ -1,11 +1,16 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<!-- 推荐加入 robots 指令,控制搜索引擎抓取 -->
|
||||||
|
<meta name="robots" content="index,follow" />
|
||||||
|
<!-- 已替换为站点域名 -->
|
||||||
|
<link rel="canonical" href="http://nba.1024x.icu/" />
|
||||||
<style>
|
<style>
|
||||||
html, body {
|
html,
|
||||||
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
/* height: 100%; */
|
/* height: 100%; */
|
||||||
@@ -16,11 +21,69 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<title>NBA在线</title>
|
<title>NBA在线观看,NBA免费直播</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="NBA在线观看,NBA免费直播,jrskan免费在线,NBA中国,NBA纬来体育,CCTV5赛事直播"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="keywords"
|
||||||
|
content="NBA在线观看,NBA免费直播,jrskan免费在线,NBA中国,NBA纬来体育,CCTV5赛事直播"
|
||||||
|
/>
|
||||||
|
<meta name="author" content="Ping" />
|
||||||
|
<!-- Open Graph / 社交分享 -->
|
||||||
|
<meta property="og:site_name" content="NBA 在线观看" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:title" content="NBA在线观看,NBA免费直播" />
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content="直播NBA赛事,在线观看高清赛事直播及赛程信息。"
|
||||||
|
/>
|
||||||
|
<meta property="og:url" content="http://jrs77.xyz/" />
|
||||||
|
<meta property="og:image" content="http://jrs77.xyz/og-image.png" />
|
||||||
|
<!-- Twitter Card -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content="NBA在线观看,NBA免费直播" />
|
||||||
|
<meta
|
||||||
|
name="twitter:description"
|
||||||
|
content="直播NBA赛事,在线观看高清赛事直播及赛程信息。"
|
||||||
|
/>
|
||||||
|
<meta name="twitter:image" content="http://jrs77.xyz/og-image.png" />
|
||||||
|
|
||||||
|
<!-- 结构化数据(JSON-LD): 简单的网站信息,便于搜索引擎理解 -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "WebSite",
|
||||||
|
"name": "NBA 在线观看",
|
||||||
|
"url": "http://jrs77.xyz/",
|
||||||
|
"potentialAction": {
|
||||||
|
"@type": "SearchAction",
|
||||||
|
"target": "http://jrs77.xyz/search?q={search_term_string}",
|
||||||
|
"query-input": "required name=search_term_string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
<script type="text/javascript" src="https://js.users.51.la/21957239.js"></script>
|
<script
|
||||||
|
charset="UTF-8"
|
||||||
|
id="LA_COLLECT"
|
||||||
|
src="//sdk.51.la/js-sdk-pro.min.js"
|
||||||
|
></script>
|
||||||
|
<script>
|
||||||
|
LA.init({ id: "KRoZweOTY55GYa3d", ck: "KRoZweOTY55GYa3d" });
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
var _hmt = _hmt || [];
|
||||||
|
(function () {
|
||||||
|
var hm = document.createElement("script");
|
||||||
|
hm.src = "https://hm.baidu.com/hm.js?5292bb429c34a0b0a70a86e8b6a9711f";
|
||||||
|
var s = document.getElementsByTagName("script")[0];
|
||||||
|
s.parentNode.insertBefore(hm, s);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
4289
package-lock.json
generated
Normal file
4289
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,11 +10,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"artplayer": "^5.3.0",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"dplayer": "^1.27.1",
|
"dplayer": "^1.27.1",
|
||||||
"element-plus": "^2.9.7",
|
"element-plus": "^2.9.7",
|
||||||
"flv.js": "^1.6.2",
|
"flv.js": "^1.6.2",
|
||||||
"hls.js": "^1.6.2",
|
"hls.js": "^1.6.2",
|
||||||
|
"md5": "^2.3.0",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
"router": "^2.2.0",
|
"router": "^2.2.0",
|
||||||
"video.js": "^8.22.0",
|
"video.js": "^8.22.0",
|
||||||
|
|||||||
61
pnpm-lock.yaml
generated
61
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
|||||||
'@element-plus/icons-vue':
|
'@element-plus/icons-vue':
|
||||||
specifier: ^2.3.1
|
specifier: ^2.3.1
|
||||||
version: 2.3.1(vue@3.5.13)
|
version: 2.3.1(vue@3.5.13)
|
||||||
|
artplayer:
|
||||||
|
specifier: ^5.3.0
|
||||||
|
version: 5.3.0
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.8.4
|
specifier: ^1.8.4
|
||||||
version: 1.8.4
|
version: 1.8.4
|
||||||
@@ -26,6 +29,9 @@ importers:
|
|||||||
hls.js:
|
hls.js:
|
||||||
specifier: ^1.6.2
|
specifier: ^1.6.2
|
||||||
version: 1.6.2
|
version: 1.6.2
|
||||||
|
md5:
|
||||||
|
specifier: ^2.3.0
|
||||||
|
version: 2.3.0
|
||||||
pinia:
|
pinia:
|
||||||
specifier: ^3.0.2
|
specifier: ^3.0.2
|
||||||
version: 3.0.2(vue@3.5.13)
|
version: 3.0.2(vue@3.5.13)
|
||||||
@@ -439,67 +445,56 @@ packages:
|
|||||||
resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==}
|
resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm-musleabihf@4.40.0':
|
'@rollup/rollup-linux-arm-musleabihf@4.40.0':
|
||||||
resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==}
|
resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-gnu@4.40.0':
|
'@rollup/rollup-linux-arm64-gnu@4.40.0':
|
||||||
resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==}
|
resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-musl@4.40.0':
|
'@rollup/rollup-linux-arm64-musl@4.40.0':
|
||||||
resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==}
|
resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-loongarch64-gnu@4.40.0':
|
'@rollup/rollup-linux-loongarch64-gnu@4.40.0':
|
||||||
resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==}
|
resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-powerpc64le-gnu@4.40.0':
|
'@rollup/rollup-linux-powerpc64le-gnu@4.40.0':
|
||||||
resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==}
|
resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-gnu@4.40.0':
|
'@rollup/rollup-linux-riscv64-gnu@4.40.0':
|
||||||
resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==}
|
resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-musl@4.40.0':
|
'@rollup/rollup-linux-riscv64-musl@4.40.0':
|
||||||
resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==}
|
resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-s390x-gnu@4.40.0':
|
'@rollup/rollup-linux-s390x-gnu@4.40.0':
|
||||||
resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==}
|
resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-gnu@4.40.0':
|
'@rollup/rollup-linux-x64-gnu@4.40.0':
|
||||||
resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==}
|
resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-musl@4.40.0':
|
'@rollup/rollup-linux-x64-musl@4.40.0':
|
||||||
resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==}
|
resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-win32-arm64-msvc@4.40.0':
|
'@rollup/rollup-win32-arm64-msvc@4.40.0':
|
||||||
resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==}
|
resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==}
|
||||||
@@ -636,6 +631,9 @@ packages:
|
|||||||
aes-decrypter@4.0.2:
|
aes-decrypter@4.0.2:
|
||||||
resolution: {integrity: sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==}
|
resolution: {integrity: sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==}
|
||||||
|
|
||||||
|
artplayer@5.3.0:
|
||||||
|
resolution: {integrity: sha512-yExO39MpEg4P+bxgChxx1eJfiUPE4q1QQRLCmqGhlsj+ANuaoEkR8hF93LdI5ZyrAcIbJkuEndxEiUoKobifDw==}
|
||||||
|
|
||||||
async-validator@4.2.5:
|
async-validator@4.2.5:
|
||||||
resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
|
resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
|
||||||
|
|
||||||
@@ -673,6 +671,9 @@ packages:
|
|||||||
caniuse-lite@1.0.30001714:
|
caniuse-lite@1.0.30001714:
|
||||||
resolution: {integrity: sha512-mtgapdwDLSSBnCI3JokHM7oEQBLxiJKVRtg10AxM1AyeiKcM96f0Mkbqeq+1AbiCtvMcHRulAAEMu693JrSWqg==}
|
resolution: {integrity: sha512-mtgapdwDLSSBnCI3JokHM7oEQBLxiJKVRtg10AxM1AyeiKcM96f0Mkbqeq+1AbiCtvMcHRulAAEMu693JrSWqg==}
|
||||||
|
|
||||||
|
charenc@0.0.2:
|
||||||
|
resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==}
|
||||||
|
|
||||||
colorjs.io@0.5.2:
|
colorjs.io@0.5.2:
|
||||||
resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==}
|
resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==}
|
||||||
|
|
||||||
@@ -691,6 +692,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
crypt@0.0.2:
|
||||||
|
resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}
|
||||||
|
|
||||||
csstype@3.1.3:
|
csstype@3.1.3:
|
||||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||||
|
|
||||||
@@ -888,6 +892,9 @@ packages:
|
|||||||
immutable@5.1.1:
|
immutable@5.1.1:
|
||||||
resolution: {integrity: sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==}
|
resolution: {integrity: sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==}
|
||||||
|
|
||||||
|
is-buffer@1.1.6:
|
||||||
|
resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
|
||||||
|
|
||||||
is-docker@3.0.0:
|
is-docker@3.0.0:
|
||||||
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
|
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
@@ -943,6 +950,10 @@ packages:
|
|||||||
jsonfile@6.1.0:
|
jsonfile@6.1.0:
|
||||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||||
|
|
||||||
|
kind-of@6.0.3:
|
||||||
|
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
kolorist@1.8.0:
|
kolorist@1.8.0:
|
||||||
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
|
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
|
||||||
|
|
||||||
@@ -972,6 +983,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
md5@2.3.0:
|
||||||
|
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
|
||||||
|
|
||||||
memoize-one@6.0.0:
|
memoize-one@6.0.0:
|
||||||
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
|
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
|
||||||
|
|
||||||
@@ -1029,6 +1043,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-zy1wx4+P3PfhXSEPJNtZmJXfhkkIaxU1VauWIrDZw1O7uJRDRJtKr9n3Ic4NgbA16KyOxOXO2ng9gYwCdXuSXA==}
|
resolution: {integrity: sha512-zy1wx4+P3PfhXSEPJNtZmJXfhkkIaxU1VauWIrDZw1O7uJRDRJtKr9n3Ic4NgbA16KyOxOXO2ng9gYwCdXuSXA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
option-validator@2.0.6:
|
||||||
|
resolution: {integrity: sha512-tmZDan2LRIRQyhUGvkff68/O0R8UmF+Btmiiz0SmSw2ng3CfPZB9wJlIjHpe/MKUZqyIZkVIXCrwr1tIN+0Dzg==}
|
||||||
|
|
||||||
parse-ms@4.0.0:
|
parse-ms@4.0.0:
|
||||||
resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
|
resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2005,6 +2022,10 @@ snapshots:
|
|||||||
global: 4.4.0
|
global: 4.4.0
|
||||||
pkcs7: 1.0.4
|
pkcs7: 1.0.4
|
||||||
|
|
||||||
|
artplayer@5.3.0:
|
||||||
|
dependencies:
|
||||||
|
option-validator: 2.0.6
|
||||||
|
|
||||||
async-validator@4.2.5: {}
|
async-validator@4.2.5: {}
|
||||||
|
|
||||||
asynckit@0.4.0: {}
|
asynckit@0.4.0: {}
|
||||||
@@ -2049,6 +2070,8 @@ snapshots:
|
|||||||
|
|
||||||
caniuse-lite@1.0.30001714: {}
|
caniuse-lite@1.0.30001714: {}
|
||||||
|
|
||||||
|
charenc@0.0.2: {}
|
||||||
|
|
||||||
colorjs.io@0.5.2: {}
|
colorjs.io@0.5.2: {}
|
||||||
|
|
||||||
combined-stream@1.0.8:
|
combined-stream@1.0.8:
|
||||||
@@ -2067,6 +2090,8 @@ snapshots:
|
|||||||
shebang-command: 2.0.0
|
shebang-command: 2.0.0
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
|
|
||||||
|
crypt@0.0.2: {}
|
||||||
|
|
||||||
csstype@3.1.3: {}
|
csstype@3.1.3: {}
|
||||||
|
|
||||||
dayjs@1.11.13: {}
|
dayjs@1.11.13: {}
|
||||||
@@ -2286,6 +2311,8 @@ snapshots:
|
|||||||
|
|
||||||
immutable@5.1.1: {}
|
immutable@5.1.1: {}
|
||||||
|
|
||||||
|
is-buffer@1.1.6: {}
|
||||||
|
|
||||||
is-docker@3.0.0: {}
|
is-docker@3.0.0: {}
|
||||||
|
|
||||||
is-function@1.0.2: {}
|
is-function@1.0.2: {}
|
||||||
@@ -2322,6 +2349,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
|
|
||||||
|
kind-of@6.0.3: {}
|
||||||
|
|
||||||
kolorist@1.8.0: {}
|
kolorist@1.8.0: {}
|
||||||
|
|
||||||
lodash-es@4.17.21: {}
|
lodash-es@4.17.21: {}
|
||||||
@@ -2350,6 +2379,12 @@ snapshots:
|
|||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
|
md5@2.3.0:
|
||||||
|
dependencies:
|
||||||
|
charenc: 0.0.2
|
||||||
|
crypt: 0.0.2
|
||||||
|
is-buffer: 1.1.6
|
||||||
|
|
||||||
memoize-one@6.0.0: {}
|
memoize-one@6.0.0: {}
|
||||||
|
|
||||||
mime-db@1.52.0: {}
|
mime-db@1.52.0: {}
|
||||||
@@ -2400,6 +2435,10 @@ snapshots:
|
|||||||
is-inside-container: 1.0.0
|
is-inside-container: 1.0.0
|
||||||
is-wsl: 3.1.0
|
is-wsl: 3.1.0
|
||||||
|
|
||||||
|
option-validator@2.0.6:
|
||||||
|
dependencies:
|
||||||
|
kind-of: 6.0.3
|
||||||
|
|
||||||
parse-ms@4.0.0: {}
|
parse-ms@4.0.0: {}
|
||||||
|
|
||||||
parseurl@1.3.3: {}
|
parseurl@1.3.3: {}
|
||||||
|
|||||||
BIN
public/imgs/spa.webp
Normal file
BIN
public/imgs/spa.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
7
public/robots.txt
Normal file
7
public/robots.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Robots.txt - allow all crawlers to index the site
|
||||||
|
# 使用实际域名
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: http://nba.1024x.icu/sitemap.xml
|
||||||
|
Host: http://nba.1024x.icu
|
||||||
20
public/sitemap.xml
Normal file
20
public/sitemap.xml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<!-- 已替换为实际域名 -->
|
||||||
|
<url>
|
||||||
|
<loc>http://nba.1024x.icu/</loc>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>http://nba.1024x.icu/lives</loc>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>http://nba.1024x.icu/test</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.5</priority>
|
||||||
|
</url>
|
||||||
|
<!-- 如需列出带参数的播放页面,请在生成 sitemap 时列出具体的 play 页面网址 -->
|
||||||
|
</urlset>
|
||||||
187
src/api/nba.js
187
src/api/nba.js
@@ -1,100 +1,173 @@
|
|||||||
import request from "./request";
|
import request from "./request";
|
||||||
import axios from 'axios'
|
import axios from "axios";
|
||||||
|
import md5 from "md5";
|
||||||
|
|
||||||
|
const SECRET_KEY = "20251125";
|
||||||
|
|
||||||
const nbaapi = axios.create({
|
const nbaapi = axios.create({
|
||||||
baseURL: 'http://api.new9.me/api',
|
baseURL: "/api",
|
||||||
// baseURL: 'http://110.42.255.182:8080',
|
timeout: 5000,
|
||||||
timeout: 2000,
|
withCredentials: true,
|
||||||
})
|
});
|
||||||
|
// const urls = async () => {
|
||||||
|
// const timestamp = Date.now();
|
||||||
|
// const sign = md5(`${timestamp}${SECRET_KEY}`);
|
||||||
|
|
||||||
const urls = async () => {
|
// return await nbaapi({
|
||||||
|
// url: '/urls',
|
||||||
|
// method: 'get',
|
||||||
|
// headers: {
|
||||||
|
// 'X-Timestamp': timestamp,
|
||||||
|
// 'X-Sign': sign,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
const urls = async (includeM3u8) => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const sign = md5(`${timestamp}${SECRET_KEY}`);
|
||||||
return await nbaapi({
|
return await nbaapi({
|
||||||
url: '/urls',
|
url: "/urls",
|
||||||
method: 'get',
|
method: "get",
|
||||||
|
headers: {
|
||||||
|
"X-Timestamp": timestamp,
|
||||||
|
"X-Sign": sign,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
includeM3u8 : includeM3u8
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response.data;
|
return response.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('获取直播URL失败:', error);
|
console.error("获取直播URL失败:", error);
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const games = async () => {
|
const games = async () => {
|
||||||
return await nbaapi({
|
return await nbaapi({
|
||||||
url: '/games',
|
url: "/games",
|
||||||
method: 'get',
|
method: "get",
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
// console.log(response.data); // 调试用
|
// console.log(response.data); // 调试用
|
||||||
return response.data; // 确保返回数据
|
return response.data; // 确保返回数据
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('获取赛事数据失败:', error);
|
console.error("获取赛事数据失败:", error);
|
||||||
throw error; // 或者返回空数组 return []
|
throw error; // 或者返回空数组 return []
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const go = async (pwd) => {
|
const go = async (pwd) => {
|
||||||
return await nbaapi({
|
return await nbaapi({
|
||||||
url: '/go',
|
url: "/go",
|
||||||
method: 'get',
|
method: "get",
|
||||||
params: {
|
params: {
|
||||||
// 这里可以添加请求参数
|
// 这里可以添加请求参数
|
||||||
pwd: pwd,
|
pwd: pwd,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
// console.log(response.data); // 调试用
|
// console.log(response.data); // 调试用
|
||||||
return response.data; // 确保返回数据
|
return response.data; // 确保返回数据
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('获取赛事数据失败:', error);
|
console.error("获取赛事数据失败:", error);
|
||||||
throw error; // 或者返回空数组 return []
|
throw error; // 或者返回空数组 return []
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const schedule = (params) => {
|
const schedule = (params) => {
|
||||||
return request({
|
return request({
|
||||||
url: '/game/schedule',
|
url: "/game/schedule",
|
||||||
method: 'get',
|
method: "get",
|
||||||
params: params,
|
params: params,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const addUrls = async (gameId, urls) => {
|
const addUrls = async (gameId, urls) => {
|
||||||
|
const payloadUrls = urls.map((item) => ({
|
||||||
|
...item,
|
||||||
|
m3u8_url: item.m3u8_url || item.url,
|
||||||
|
}));
|
||||||
|
|
||||||
return await nbaapi({
|
return await nbaapi({
|
||||||
url: '/addUrls',
|
url: "/addUrls",
|
||||||
method: 'post',
|
method: "post",
|
||||||
data: {
|
data: {
|
||||||
gameId: gameId,
|
gameId: gameId,
|
||||||
urls: urls
|
urls: payloadUrls,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response.data;
|
return response.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('添加直播URL失败:', error);
|
console.error("添加直播URL失败:", error);
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 在nba.js中添加删除函数
|
// 在nba.js中添加删除函数
|
||||||
const deleteUrlById = async (id) => {
|
const deleteUrlById = async (id) => {
|
||||||
return await nbaapi({
|
return await nbaapi({
|
||||||
url: `/delete/${id}`,
|
url: `/delete/${id}`,
|
||||||
method: 'get',
|
method: "get",
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return response.data;
|
return response.data;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('删除直播URL失败:', error);
|
console.error("删除直播URL失败:", error);
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 修改直播链接
|
||||||
|
const updateUrlById = async ({ id, url, gameId, type }) => {
|
||||||
|
return await nbaapi({
|
||||||
|
url: "/update",
|
||||||
|
method: "post",
|
||||||
|
data: {
|
||||||
|
id,
|
||||||
|
url,
|
||||||
|
gameId,
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
return response.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("修改直播URL失败:", error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export { schedule, games, urls, go,addUrls,deleteUrlById };
|
// 按需获取单个直播源的播放地址
|
||||||
|
const fetchLiveUrl = async (gameId, type) => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const sign = md5(`${timestamp}${SECRET_KEY}`);
|
||||||
|
|
||||||
|
return await nbaapi({
|
||||||
|
url: "/live/url",
|
||||||
|
method: "get",
|
||||||
|
headers: {
|
||||||
|
"X-Timestamp": timestamp,
|
||||||
|
"X-Sign": sign,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
gameId,
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((response) => response.data)
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("获取直播源URL失败:", error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { schedule, games, urls, go, addUrls, deleteUrlById, updateUrlById, fetchLiveUrl };
|
||||||
|
|||||||
70
src/api/userApi.js
Normal file
70
src/api/userApi.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const userApi = axios.create({
|
||||||
|
// baseURL: "https://api9.jrs77.xyz/user",
|
||||||
|
timeout: 5000,
|
||||||
|
// baseURL: "http://154.36.154.211:9001/user",
|
||||||
|
// baseURL: "http://127.0.0.1:9001/user",
|
||||||
|
// baseURL: "http://116.62.173.2:9001/user",
|
||||||
|
baseURL: "/user",
|
||||||
|
withCredentials: true
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// 发送验证码(type: 1 注册 / 2 修改密码)
|
||||||
|
export const sendRegisterCode = async (email, type = 1) => {
|
||||||
|
return userApi({
|
||||||
|
url: "/send",
|
||||||
|
method: "get",
|
||||||
|
params: { email, type },
|
||||||
|
}).then((res) => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 注册
|
||||||
|
export const registerUser = async ({ email, password, code, username = "" }) => {
|
||||||
|
return userApi({
|
||||||
|
url: "/register",
|
||||||
|
method: "post",
|
||||||
|
data: { email, password, code, username },
|
||||||
|
}).then((res) => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 登录(推荐用 form-data 提交给 Spring)
|
||||||
|
export const loginUser = async ({ email, password }) => {
|
||||||
|
const form = new URLSearchParams();
|
||||||
|
form.append("email", email);
|
||||||
|
form.append("password", password);
|
||||||
|
|
||||||
|
return userApi({
|
||||||
|
url: "/login",
|
||||||
|
method: "post",
|
||||||
|
data: form,
|
||||||
|
}).then((res) => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取当前登录用户(需要后端提供 /user/me)
|
||||||
|
export const getCurrentUser = async () => {
|
||||||
|
return userApi({
|
||||||
|
url: "/me",
|
||||||
|
method: "get",
|
||||||
|
}).then((res) => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 退出登录(需要后端提供 /user/logout)
|
||||||
|
export const logoutUser = async () => {
|
||||||
|
return userApi({
|
||||||
|
url: "/logout",
|
||||||
|
method: "post",
|
||||||
|
}).then((res) => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
//添加修改密码接口
|
||||||
|
export const updatePassword = async ({ email, password, code}) => {
|
||||||
|
return userApi({
|
||||||
|
url: "/update",
|
||||||
|
method: "post",
|
||||||
|
data: { email, password, code},
|
||||||
|
}).then((res) => res.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default userApi;
|
||||||
BIN
src/assets/imgs/qw.jpg
Normal file
BIN
src/assets/imgs/qw.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 243 KiB |
BIN
src/assets/imgs/zfb.jpg
Normal file
BIN
src/assets/imgs/zfb.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
365
src/components/AuthModal - 副本.vue
Normal file
365
src/components/AuthModal - 副本.vue
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
<template>
|
||||||
|
<div class="auth-overlay" @click.self="close">
|
||||||
|
<div class="auth-modal">
|
||||||
|
<header class="auth-header">
|
||||||
|
<div class="tabs">
|
||||||
|
<button
|
||||||
|
:class="{ active: mode === 'login' }"
|
||||||
|
@click="switchMode('login')"
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="{ active: mode === 'register' }"
|
||||||
|
@click="switchMode('register')"
|
||||||
|
>
|
||||||
|
注册
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="{ active: mode === 'reset' }"
|
||||||
|
@click="switchMode('reset')"
|
||||||
|
>
|
||||||
|
修改密码
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button class="close-btn" @click="close">×</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleSubmit" class="auth-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>邮箱</label>
|
||||||
|
<div class="email-row">
|
||||||
|
<input
|
||||||
|
v-model="form.email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
placeholder="请输入邮箱"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="mode !== 'login'"
|
||||||
|
type="button"
|
||||||
|
class="code-btn"
|
||||||
|
:disabled="countdown > 0"
|
||||||
|
@click="handleSendCode"
|
||||||
|
>
|
||||||
|
{{ countdown > 0 ? countdown + ' 秒后重发' : '发送验证码' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-if="mode !== 'login'">
|
||||||
|
<label>邮箱验证码</label>
|
||||||
|
<input
|
||||||
|
v-model="form.code"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="请输入邮箱验证码"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{ mode === 'reset' ? '新密码' : '密码' }}</label>
|
||||||
|
<input
|
||||||
|
v-model="form.password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
placeholder="请输入密码"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-if="mode !== 'login'">
|
||||||
|
<label>确认密码</label>
|
||||||
|
<input
|
||||||
|
v-model="form.confirm"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
placeholder="请再次输入密码"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button type="submit" class="primary-btn">
|
||||||
|
{{
|
||||||
|
mode === 'login'
|
||||||
|
? '登录'
|
||||||
|
: mode === 'register'
|
||||||
|
? '注册'
|
||||||
|
: '修改密码'
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="ghost-btn" @click="close">
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref, watch, onUnmounted } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
defaultMode: {
|
||||||
|
type: String,
|
||||||
|
default: "login",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue", "submit", "send-code"]);
|
||||||
|
|
||||||
|
const countdown = ref(0);
|
||||||
|
let timer = null;
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
confirm: "",
|
||||||
|
code: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const mode = ref(props.defaultMode || "login");
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.defaultMode,
|
||||||
|
() => {
|
||||||
|
resetForm();
|
||||||
|
mode.value = props.defaultMode || "login";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
form.email = "";
|
||||||
|
form.password = "";
|
||||||
|
form.confirm = "";
|
||||||
|
form.code = "";
|
||||||
|
countdown.value = 0;
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchMode = (target) => {
|
||||||
|
if (target !== mode.value) {
|
||||||
|
mode.value = target;
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
resetForm();
|
||||||
|
mode.value = props.defaultMode || "login";
|
||||||
|
emit("update:modelValue", false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isEmailValid = (email) => {
|
||||||
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!isEmailValid(form.email)) {
|
||||||
|
alert("邮箱格式不正确");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mode.value !== "login" && form.password !== form.confirm) {
|
||||||
|
alert("两次输入的密码不一致");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mode.value !== "login" && !form.code) {
|
||||||
|
alert("请输入邮箱验证码");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit("submit", {
|
||||||
|
mode: mode.value,
|
||||||
|
email: form.email,
|
||||||
|
password: form.password,
|
||||||
|
code: form.code,
|
||||||
|
});
|
||||||
|
resetForm();
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendCode = () => {
|
||||||
|
if (countdown.value > 0) {
|
||||||
|
// 在倒计时中,直接忽略点击
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!form.email) {
|
||||||
|
alert("请先输入邮箱");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isEmailValid(form.email)) {
|
||||||
|
alert("邮箱格式不正确");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知父组件发送验证码
|
||||||
|
emit("send-code", { email: form.email, mode: mode.value });
|
||||||
|
|
||||||
|
// 这里默认认为发送成功,如果你要等后端返回再开始倒计时,
|
||||||
|
// 可以把下面这段逻辑放到父组件里,再通过 props 传回当前组件。
|
||||||
|
// alert("验证码发送成功");
|
||||||
|
countdown.value = 60;
|
||||||
|
timer = setInterval(() => {
|
||||||
|
countdown.value--;
|
||||||
|
if (countdown.value <= 0) {
|
||||||
|
countdown.value = 0;
|
||||||
|
clearInterval(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// const handleSendCode = () => {
|
||||||
|
// if (!form.email) {
|
||||||
|
// alert("请先输入邮箱");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// if (!isEmailValid(form.email)) {
|
||||||
|
//
|
||||||
|
("邮箱格式不正确");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// emit("send-code", { email: form.email });
|
||||||
|
// };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.code-btn[disabled] {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-modal {
|
||||||
|
width: 360px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.18);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs button {
|
||||||
|
padding: 8px 14px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f2f4f7;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs button.active {
|
||||||
|
background: #1d428a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-row input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-btn {
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 1px solid #1d428a;
|
||||||
|
background: #1d428a;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn,
|
||||||
|
.ghost-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
background: #1d428a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost-btn {
|
||||||
|
background: #f2f4f7;
|
||||||
|
color: #1d428a;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
365
src/components/AuthModal.vue
Normal file
365
src/components/AuthModal.vue
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
<template>
|
||||||
|
<div class="auth-overlay" @click.self="close">
|
||||||
|
<div class="auth-modal">
|
||||||
|
<header class="auth-header">
|
||||||
|
<div class="tabs">
|
||||||
|
<button
|
||||||
|
:class="{ active: mode === 'login' }"
|
||||||
|
@click="switchMode('login')"
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="{ active: mode === 'register' }"
|
||||||
|
@click="switchMode('register')"
|
||||||
|
>
|
||||||
|
注册
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="{ active: mode === 'reset' }"
|
||||||
|
@click="switchMode('reset')"
|
||||||
|
>
|
||||||
|
修改密码
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button class="close-btn" @click="close">×</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleSubmit" class="auth-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>邮箱</label>
|
||||||
|
<div class="email-row">
|
||||||
|
<input
|
||||||
|
v-model="form.email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
placeholder="请输入邮箱"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="mode !== 'login'"
|
||||||
|
type="button"
|
||||||
|
class="code-btn"
|
||||||
|
:disabled="countdown > 0"
|
||||||
|
@click="handleSendCode"
|
||||||
|
>
|
||||||
|
{{ countdown > 0 ? countdown + ' 秒后重发' : '发送验证码' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-if="mode !== 'login'">
|
||||||
|
<label>邮箱验证码</label>
|
||||||
|
<input
|
||||||
|
v-model="form.code"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="请输入邮箱验证码"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{ mode === 'reset' ? '新密码' : '密码' }}</label>
|
||||||
|
<input
|
||||||
|
v-model="form.password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
placeholder="请输入密码"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" v-if="mode !== 'login'">
|
||||||
|
<label>确认密码</label>
|
||||||
|
<input
|
||||||
|
v-model="form.confirm"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
placeholder="请再次输入密码"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button type="submit" class="primary-btn">
|
||||||
|
{{
|
||||||
|
mode === 'login'
|
||||||
|
? '登录'
|
||||||
|
: mode === 'register'
|
||||||
|
? '注册'
|
||||||
|
: '修改密码'
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="ghost-btn" @click="close">
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref, watch, onUnmounted } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
defaultMode: {
|
||||||
|
type: String,
|
||||||
|
default: "login",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["update:modelValue", "submit", "send-code"]);
|
||||||
|
|
||||||
|
const countdown = ref(0);
|
||||||
|
let timer = null;
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
confirm: "",
|
||||||
|
code: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const mode = ref(props.defaultMode || "login");
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.defaultMode,
|
||||||
|
() => {
|
||||||
|
resetForm();
|
||||||
|
mode.value = props.defaultMode || "login";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
form.email = "";
|
||||||
|
form.password = "";
|
||||||
|
form.confirm = "";
|
||||||
|
form.code = "";
|
||||||
|
countdown.value = 0;
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchMode = (target) => {
|
||||||
|
if (target !== mode.value) {
|
||||||
|
mode.value = target;
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
resetForm();
|
||||||
|
mode.value = props.defaultMode || "login";
|
||||||
|
emit("update:modelValue", false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isEmailValid = (email) => {
|
||||||
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!isEmailValid(form.email)) {
|
||||||
|
alert("邮箱格式不正确");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mode.value !== "login" && form.password !== form.confirm) {
|
||||||
|
alert("两次输入的密码不一致");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mode.value !== "login" && !form.code) {
|
||||||
|
alert("请输入邮箱验证码");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit("submit", {
|
||||||
|
mode: mode.value,
|
||||||
|
email: form.email,
|
||||||
|
password: form.password,
|
||||||
|
code: form.code,
|
||||||
|
});
|
||||||
|
resetForm();
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendCode = () => {
|
||||||
|
if (countdown.value > 0) {
|
||||||
|
// 在倒计时中,直接忽略点击
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!form.email) {
|
||||||
|
alert("请先输入邮箱");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isEmailValid(form.email)) {
|
||||||
|
alert("邮箱格式不正确");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知父组件发送验证码
|
||||||
|
emit("send-code", { email: form.email, mode: mode.value });
|
||||||
|
|
||||||
|
// 这里默认认为发送成功,如果你要等后端返回再开始倒计时,
|
||||||
|
// 可以把下面这段逻辑放到父组件里,再通过 props 传回当前组件。
|
||||||
|
// alert("验证码发送成功");
|
||||||
|
countdown.value = 60;
|
||||||
|
timer = setInterval(() => {
|
||||||
|
countdown.value--;
|
||||||
|
if (countdown.value <= 0) {
|
||||||
|
countdown.value = 0;
|
||||||
|
clearInterval(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// const handleSendCode = () => {
|
||||||
|
// if (!form.email) {
|
||||||
|
// alert("请先输入邮箱");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// if (!isEmailValid(form.email)) {
|
||||||
|
//
|
||||||
|
("邮箱格式不正确");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// emit("send-code", { email: form.email });
|
||||||
|
// };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.code-btn[disabled] {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 2000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-modal {
|
||||||
|
width: 360px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.18);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs button {
|
||||||
|
padding: 8px 14px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f2f4f7;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs button.active {
|
||||||
|
background: #1d428a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-row input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-btn {
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 1px solid #1d428a;
|
||||||
|
background: #1d428a;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn,
|
||||||
|
.ghost-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
background: #1d428a;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost-btn {
|
||||||
|
background: #f2f4f7;
|
||||||
|
color: #1d428a;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
438
src/components/LiveStream - 副本.vue
Normal file
438
src/components/LiveStream - 副本.vue
Normal file
@@ -0,0 +1,438 @@
|
|||||||
|
<template>
|
||||||
|
<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">
|
||||||
|
<p>{{ gameData.awayTeam.name }}</p>
|
||||||
|
<span>{{ gameData.awayTeam.record }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vs-circle">VS</div>
|
||||||
|
|
||||||
|
<div class="team-info home-team">
|
||||||
|
<div class="team-details">
|
||||||
|
<p>{{ gameData.homeTeam.name }}</p>
|
||||||
|
<span>{{ gameData.homeTeam.record }}</span>
|
||||||
|
</div>
|
||||||
|
<img :src="gameData.homeTeam.logo" :alt="gameData.homeTeam.name" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="video-container">
|
||||||
|
<video
|
||||||
|
id="native-player"
|
||||||
|
ref="videoRef"
|
||||||
|
class="native-video"
|
||||||
|
controls
|
||||||
|
playsinline
|
||||||
|
webkit-playsinline
|
||||||
|
x5-video-player-type="h5-page"
|
||||||
|
x5-video-player-fullscreen="true"
|
||||||
|
>
|
||||||
|
您的浏览器不支持 HTML5 video 标签。
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stream-switcher" v-if="allStreams.length > 0">
|
||||||
|
<div class="stream-buttons">
|
||||||
|
<button
|
||||||
|
v-for="stream in allStreams"
|
||||||
|
:key="stream.type"
|
||||||
|
@click="switchStream(stream)"
|
||||||
|
:class="{ active: currentStream?.type === stream.type }"
|
||||||
|
>
|
||||||
|
{{ getStreamName(stream.type) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="loading" class="loading-tip">正在获取播放地址...</div>
|
||||||
|
<div v-if="loadError" class="error-tip">{{ loadError }}</div>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: center;">
|
||||||
|
<h5>出现黑屏、无法播放等问题时,建议切换浏览器或刷新页面
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<button class="back-button" @click="goBack">← 返回赛程</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onBeforeUnmount, computed, nextTick } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { useGameStore } from "@/stores/game";
|
||||||
|
import { fetchLiveUrl } from "@/api/nba";
|
||||||
|
import Hls from "hls.js"; // 仅保留 Hls.js 用于 PC 端
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const gameStore = useGameStore();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const loadError = ref("");
|
||||||
|
const videoRef = ref(null); // Video 标签引用
|
||||||
|
let hlsInstance = null; // Hls 实例
|
||||||
|
|
||||||
|
const gameData = computed(() => gameStore.currentGame);
|
||||||
|
const allStreams = computed(() => gameStore.allStreams);
|
||||||
|
const gameId = computed(() => gameStore.gameId);
|
||||||
|
const currentStream = computed({
|
||||||
|
get: () => gameStore.currentStream,
|
||||||
|
set: (val) => (gameStore.currentStream = val),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化播放器核心逻辑
|
||||||
|
const initPlayer = () => {
|
||||||
|
if (!currentStream.value?.url) return;
|
||||||
|
const url = currentStream.value.url;
|
||||||
|
const video = videoRef.value;
|
||||||
|
|
||||||
|
loadError.value = "";
|
||||||
|
|
||||||
|
// 1. 清理旧实例
|
||||||
|
destroyPlayer();
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
if (!video) return;
|
||||||
|
console.log("正在初始化播放:", url);
|
||||||
|
|
||||||
|
// 2. 移动端/Safari 原生支持 m3u8 (投屏兼容性最佳)
|
||||||
|
if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
||||||
|
video.src = url;
|
||||||
|
video.load();
|
||||||
|
|
||||||
|
video.play().catch(e => {
|
||||||
|
console.log("自动播放被拦截,切换静音播放", e);
|
||||||
|
video.muted = true;
|
||||||
|
video.play();
|
||||||
|
});
|
||||||
|
|
||||||
|
video.onerror = () => {
|
||||||
|
loadError.value = "播放失败,请尝试切换其他源";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 3. PC端 (Chrome/Edge) 使用 Hls.js
|
||||||
|
else if (Hls.isSupported()) {
|
||||||
|
hlsInstance = new Hls({
|
||||||
|
enableWorker: true,
|
||||||
|
lowLatencyMode: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
hlsInstance.loadSource(url);
|
||||||
|
hlsInstance.attachMedia(video);
|
||||||
|
|
||||||
|
hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||||
|
video.play().catch(e => {
|
||||||
|
video.muted = true;
|
||||||
|
video.play();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 错误重连机制
|
||||||
|
hlsInstance.on(Hls.Events.ERROR, function (event, data) {
|
||||||
|
if (data.fatal) {
|
||||||
|
switch (data.type) {
|
||||||
|
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||||
|
console.log("网络错误(可能是Token过期),尝试重连...");
|
||||||
|
hlsInstance.destroy();
|
||||||
|
// 触发重新获取 URL 逻辑
|
||||||
|
const currentType = currentStream.value.type;
|
||||||
|
currentStream.value.url = null;
|
||||||
|
switchStream({ type: currentType });
|
||||||
|
break;
|
||||||
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||||
|
hlsInstance.recoverMediaError();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
hlsInstance.destroy();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
loadError.value = "您的浏览器不支持播放此视频";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const destroyPlayer = () => {
|
||||||
|
if (hlsInstance) {
|
||||||
|
hlsInstance.destroy();
|
||||||
|
hlsInstance = null;
|
||||||
|
}
|
||||||
|
if (videoRef.value) {
|
||||||
|
const video = videoRef.value;
|
||||||
|
video.pause();
|
||||||
|
video.removeAttribute('src');
|
||||||
|
video.load();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureStreamUrl = async (stream) => {
|
||||||
|
if (!stream || !gameId.value) return null;
|
||||||
|
if (stream.url) return stream;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
loadError.value = "";
|
||||||
|
try {
|
||||||
|
const resp = await fetchLiveUrl(gameId.value, stream.type);
|
||||||
|
const url =
|
||||||
|
resp?.url ||
|
||||||
|
resp?.data?.url ||
|
||||||
|
resp?.data?.data ||
|
||||||
|
(typeof resp === "string" ? resp : null);
|
||||||
|
if (!url) throw new Error("未返回播放地址");
|
||||||
|
return { ...stream, url };
|
||||||
|
} catch (err) {
|
||||||
|
console.error("获取直播地址失败:", err);
|
||||||
|
loadError.value = "获取直播地址失败,请重试或切换其他源";
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchStream = async (stream) => {
|
||||||
|
const streamWithUrl = await ensureStreamUrl(stream);
|
||||||
|
if (!streamWithUrl) return;
|
||||||
|
loadError.value = "";
|
||||||
|
currentStream.value = streamWithUrl;
|
||||||
|
initPlayer();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStreamName = (type) => {
|
||||||
|
const names = {
|
||||||
|
tx: "企鹅超清",
|
||||||
|
wl: "纬来体育",
|
||||||
|
nba: "高清原声",
|
||||||
|
mg: "咪咕高清",
|
||||||
|
zb: "高清直播",
|
||||||
|
};
|
||||||
|
return names[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
gameStore.clearGameData();
|
||||||
|
router.go(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (allStreams.value.length > 0 && !currentStream.value) {
|
||||||
|
currentStream.value = allStreams.value[0];
|
||||||
|
}
|
||||||
|
const initial = currentStream.value || allStreams.value[0];
|
||||||
|
if (initial) {
|
||||||
|
switchStream(initial);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
destroyPlayer();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.live-stream-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #f5f5f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 40px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-details {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-circle {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background: #f29155;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 核心播放器容器样式 */
|
||||||
|
.video-container {
|
||||||
|
width: 100%;
|
||||||
|
/* 16:9 比例容器 */
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
position: relative;
|
||||||
|
background: #000;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 原生 Video 标签样式 */
|
||||||
|
/* 原生 Video 标签样式 */
|
||||||
|
.native-video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
/* === 新增:PC端 隐藏进度条和时间 === */
|
||||||
|
|
||||||
|
/* 隐藏进度条 (滑动条) */
|
||||||
|
&::-webkit-media-controls-timeline {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏当前播放时间 (例如 00:15) */
|
||||||
|
&::-webkit-media-controls-current-time-display {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏剩余时间 (例如 -02:30) */
|
||||||
|
&::-webkit-media-controls-time-remaining-display {
|
||||||
|
display: none !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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-tip {
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-tip {
|
||||||
|
margin-top: 8px;
|
||||||
|
text-align: center;
|
||||||
|
color: #d9534f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
display: block;
|
||||||
|
width: 200px;
|
||||||
|
margin: 10px 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) {
|
||||||
|
.game-header {
|
||||||
|
gap: 20px;
|
||||||
|
.team-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-details {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-circle {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="live-stream-container">
|
<div class="live-stream-container">
|
||||||
<!-- 比赛信息 -->
|
|
||||||
<div class="game-header" v-if="gameData">
|
<div class="game-header" v-if="gameData">
|
||||||
<div class="team-info away-team">
|
<div class="team-info away-team">
|
||||||
<img :src="gameData.awayTeam.logo" :alt="gameData.awayTeam.name" />
|
<img :src="gameData.awayTeam.logo" :alt="gameData.awayTeam.name" />
|
||||||
<div class="team-details">
|
<div class="team-details">
|
||||||
<!-- <h3>{{ gameData.awayTeam.city }}</h3> -->
|
|
||||||
<p>{{ gameData.awayTeam.name }}</p>
|
<p>{{ gameData.awayTeam.name }}</p>
|
||||||
<span>{{ gameData.awayTeam.record }}</span>
|
<span>{{ gameData.awayTeam.record }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -14,9 +12,7 @@
|
|||||||
<div class="vs-circle">VS</div>
|
<div class="vs-circle">VS</div>
|
||||||
|
|
||||||
<div class="team-info home-team">
|
<div class="team-info home-team">
|
||||||
|
|
||||||
<div class="team-details">
|
<div class="team-details">
|
||||||
<!-- <h3>{{ gameData.homeTeam.city }}</h3> -->
|
|
||||||
<p>{{ gameData.homeTeam.name }}</p>
|
<p>{{ gameData.homeTeam.name }}</p>
|
||||||
<span>{{ gameData.homeTeam.record }}</span>
|
<span>{{ gameData.homeTeam.record }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -24,12 +20,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 播放器 -->
|
<div class="video-container">
|
||||||
<div id="dplayer-live" class="dplayer-container"></div>
|
<video
|
||||||
|
id="native-player"
|
||||||
|
ref="videoRef"
|
||||||
|
class="native-video"
|
||||||
|
controls
|
||||||
|
playsinline
|
||||||
|
webkit-playsinline
|
||||||
|
x5-video-player-type="h5-page"
|
||||||
|
x5-video-player-fullscreen="true"
|
||||||
|
>
|
||||||
|
您的浏览器不支持 HTML5 video 标签。
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 直播源列表(始终显示) -->
|
|
||||||
<div class="stream-switcher" v-if="allStreams.length > 0">
|
<div class="stream-switcher" v-if="allStreams.length > 0">
|
||||||
<!-- <h3>直播源</h3> -->
|
|
||||||
<div class="stream-buttons">
|
<div class="stream-buttons">
|
||||||
<button
|
<button
|
||||||
v-for="stream in allStreams"
|
v-for="stream in allStreams"
|
||||||
@@ -40,164 +46,189 @@
|
|||||||
{{ getStreamName(stream.type) }}
|
{{ getStreamName(stream.type) }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="loading" class="loading-tip">正在获取播放地址...</div>
|
||||||
|
<div v-if="loadError" class="error-tip">{{ loadError }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display: flex; justify-content: center;align-items: center;
|
||||||
<!-- 返回按钮 -->
|
flex-direction: column;">
|
||||||
|
<p style="font-size: 15px;">推荐使用谷歌浏览器观看</p>
|
||||||
|
<p style="font-size: 15px;">出现卡顿,推荐开启VPN选择美国、香港节点观看</p>
|
||||||
|
<p style="font-size: 15px;">请不要相信、访问视频中广告站点,谨防钓鱼诈骗!</p>
|
||||||
|
</div>
|
||||||
<button class="back-button" @click="goBack">← 返回赛程</button>
|
<button class="back-button" @click="goBack">← 返回赛程</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onBeforeUnmount, computed } from "vue";
|
import { ref, onMounted, onBeforeUnmount, computed, nextTick } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { useGameStore } from "@/stores/game";
|
import { useGameStore } from "@/stores/game";
|
||||||
import DPlayer from "dplayer";
|
import { fetchLiveUrl } from "@/api/nba";
|
||||||
import Hls from "hls.js";
|
import Hls from "hls.js"; // 仅保留 Hls.js 用于 PC 端
|
||||||
import Flv from "flv.js";
|
|
||||||
|
|
||||||
// 注册全局变量
|
|
||||||
window.flvjs = Flv;
|
|
||||||
window.Hls = Hls;
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const gameStore = useGameStore();
|
const gameStore = useGameStore();
|
||||||
|
|
||||||
const dpInstance = ref(null);
|
const loading = ref(false);
|
||||||
|
const loadError = ref("");
|
||||||
|
const videoRef = ref(null); // Video 标签引用
|
||||||
|
let hlsInstance = null; // Hls 实例
|
||||||
|
|
||||||
// 从store获取数据
|
|
||||||
const gameData = computed(() => gameStore.currentGame);
|
const gameData = computed(() => gameStore.currentGame);
|
||||||
const allStreams = computed(() => gameStore.allStreams);
|
const allStreams = computed(() => gameStore.allStreams);
|
||||||
|
const gameId = computed(() => gameStore.gameId);
|
||||||
const currentStream = computed({
|
const currentStream = computed({
|
||||||
get: () => gameStore.currentStream,
|
get: () => gameStore.currentStream,
|
||||||
set: (val) => (gameStore.currentStream = val),
|
set: (val) => (gameStore.currentStream = val),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 初始化播放器
|
// 初始化播放器核心逻辑
|
||||||
// const initPlayer = () => {
|
|
||||||
// if (dpInstance.value) {
|
|
||||||
// dpInstance.value.destroy();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// dpInstance.value = new DPlayer({
|
|
||||||
// container: document.getElementById('dplayer-live'),
|
|
||||||
// live: true,
|
|
||||||
// autoplay: true,
|
|
||||||
// video: {
|
|
||||||
// url: currentStream.value?.url || '',
|
|
||||||
// type: 'auto'
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // 强制设置视频尺寸
|
|
||||||
// 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 initPlayer = () => {
|
const initPlayer = () => {
|
||||||
if (!currentStream.value?.url) return;
|
if (!currentStream.value?.url) return;
|
||||||
|
const url = currentStream.value.url;
|
||||||
|
const video = videoRef.value;
|
||||||
|
|
||||||
// 销毁旧实例
|
loadError.value = "";
|
||||||
if (dpInstance.value) {
|
|
||||||
dpInstance.value.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
dpInstance.value = new DPlayer({
|
// 1. 清理旧实例
|
||||||
container: document.getElementById("dplayer-live"),
|
destroyPlayer();
|
||||||
live: true,
|
|
||||||
autoplay: true,
|
|
||||||
airplay: true,
|
|
||||||
video: {
|
|
||||||
url: currentStream.value.url,
|
|
||||||
type: "custom", // 修改为custom类型
|
|
||||||
customType: {
|
|
||||||
custom: function (video, player) {
|
|
||||||
const url = video.src;
|
|
||||||
|
|
||||||
// 自动检测协议类型
|
nextTick(() => {
|
||||||
if (url.includes(".m3u8") || url.endsWith("/hls")) {
|
if (!video) return;
|
||||||
const hls = new Hls();
|
console.log("正在初始化播放:", url);
|
||||||
hls.loadSource(url);
|
|
||||||
hls.attachMedia(video);
|
|
||||||
player.on("destroy", () => hls.destroy());
|
|
||||||
} else if (url.includes(".flv") || url.endsWith("/flv")) {
|
|
||||||
const flv = Flv.createPlayer({ type: "flv", url });
|
|
||||||
flv.attachMediaElement(video);
|
|
||||||
flv.load();
|
|
||||||
player.on("destroy", () => flv.destroy());
|
|
||||||
} else {
|
|
||||||
// 其他协议回退到DPlayer默认处理
|
|
||||||
video.src = url;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 保持原有尺寸调整逻辑
|
// 2. 移动端/Safari 原生支持 m3u8 (投屏兼容性最佳)
|
||||||
setTimeout(() => {
|
if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
||||||
const video = document
|
video.src = url;
|
||||||
.getElementById("dplayer-live")
|
video.load();
|
||||||
?.querySelector("video");
|
|
||||||
if (video) {
|
video.play().catch(e => {
|
||||||
video.style.cssText = `
|
console.log("自动播放被拦截,切换静音播放", e);
|
||||||
width: 100% !important;
|
video.muted = true;
|
||||||
height: 100% !important;
|
video.play();
|
||||||
object-fit: contain !important;
|
});
|
||||||
`;
|
|
||||||
|
video.onerror = () => {
|
||||||
|
loadError.value = "播放失败,请尝试切换其他源";
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}, 500);
|
// 3. PC端 (Chrome/Edge) 使用 Hls.js
|
||||||
|
else if (Hls.isSupported()) {
|
||||||
|
hlsInstance = new Hls({
|
||||||
|
enableWorker: true,
|
||||||
|
lowLatencyMode: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
hlsInstance.loadSource(url);
|
||||||
|
hlsInstance.attachMedia(video);
|
||||||
|
|
||||||
|
hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||||
|
video.play().catch(e => {
|
||||||
|
video.muted = true;
|
||||||
|
video.play();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 错误重连机制
|
||||||
|
hlsInstance.on(Hls.Events.ERROR, function (event, data) {
|
||||||
|
if (data.fatal) {
|
||||||
|
switch (data.type) {
|
||||||
|
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||||
|
console.log("网络错误(可能是Token过期),尝试重连...");
|
||||||
|
hlsInstance.destroy();
|
||||||
|
// 触发重新获取 URL 逻辑
|
||||||
|
const currentType = currentStream.value.type;
|
||||||
|
currentStream.value.url = null;
|
||||||
|
switchStream({ type: currentType });
|
||||||
|
break;
|
||||||
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||||
|
hlsInstance.recoverMediaError();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
hlsInstance.destroy();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
loadError.value = "您的浏览器不支持播放此视频";
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切换直播源
|
const destroyPlayer = () => {
|
||||||
const switchStream = (stream) => {
|
if (hlsInstance) {
|
||||||
currentStream.value = stream;
|
hlsInstance.destroy();
|
||||||
|
hlsInstance = null;
|
||||||
|
}
|
||||||
|
if (videoRef.value) {
|
||||||
|
const video = videoRef.value;
|
||||||
|
video.pause();
|
||||||
|
video.removeAttribute('src');
|
||||||
|
video.load();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureStreamUrl = async (stream) => {
|
||||||
|
if (!stream || !gameId.value) return null;
|
||||||
|
if (stream.url) return stream;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
loadError.value = "";
|
||||||
|
try {
|
||||||
|
const resp = await fetchLiveUrl(gameId.value, stream.type);
|
||||||
|
const url =
|
||||||
|
resp?.url ||
|
||||||
|
resp?.data?.url ||
|
||||||
|
resp?.data?.data ||
|
||||||
|
(typeof resp === "string" ? resp : null);
|
||||||
|
if (!url) throw new Error("未返回播放地址");
|
||||||
|
return { ...stream, url };
|
||||||
|
} catch (err) {
|
||||||
|
console.error("获取直播地址失败:", err);
|
||||||
|
loadError.value = "获取直播地址失败,请重试或切换其他源";
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchStream = async (stream) => {
|
||||||
|
const streamWithUrl = await ensureStreamUrl(stream);
|
||||||
|
if (!streamWithUrl) return;
|
||||||
|
loadError.value = "";
|
||||||
|
currentStream.value = streamWithUrl;
|
||||||
initPlayer();
|
initPlayer();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取直播源名称
|
|
||||||
const getStreamName = (type) => {
|
const getStreamName = (type) => {
|
||||||
const names = {
|
const names = {
|
||||||
tx: "企鹅体育",
|
tx: "企鹅超清",
|
||||||
wl: "纬来体育",
|
wl: "纬来体育",
|
||||||
mg: "咪咕体育",
|
|
||||||
nba: "高清原声",
|
nba: "高清原声",
|
||||||
|
mg: "咪咕高清",
|
||||||
zb: "高清直播",
|
zb: "高清直播",
|
||||||
};
|
};
|
||||||
return names[type] || type;
|
return names[type] || type;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 返回赛程页
|
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
gameStore.clearGameData();
|
gameStore.clearGameData();
|
||||||
router.go(-1);
|
router.go(-1);
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 默认选择第一个直播源
|
|
||||||
if (allStreams.value.length > 0 && !currentStream.value) {
|
if (allStreams.value.length > 0 && !currentStream.value) {
|
||||||
currentStream.value = allStreams.value[0];
|
currentStream.value = allStreams.value[0];
|
||||||
}
|
}
|
||||||
initPlayer();
|
const initial = currentStream.value || allStreams.value[0];
|
||||||
|
if (initial) {
|
||||||
|
switchStream(initial);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (dpInstance.value) {
|
destroyPlayer();
|
||||||
dpInstance.value.destroy();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -205,7 +236,6 @@ onBeforeUnmount(() => {
|
|||||||
.live-stream-container {
|
.live-stream-container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
// padding: 20px;
|
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
@@ -237,12 +267,6 @@ onBeforeUnmount(() => {
|
|||||||
.team-details {
|
.team-details {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
@@ -269,19 +293,46 @@ onBeforeUnmount(() => {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dplayer-container {
|
/* 核心播放器容器样式 */
|
||||||
|
.video-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
/* 16:9 比例容器 */
|
||||||
aspect-ratio: 16/9;
|
aspect-ratio: 16/9;
|
||||||
position: relative;
|
position: relative;
|
||||||
background: #000;
|
background: #000;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
::v-deep {
|
/* 原生 Video 标签样式 */
|
||||||
.dplayer-video {
|
/* 原生 Video 标签样式 */
|
||||||
width: 100% !important;
|
.native-video {
|
||||||
height: 100% !important;
|
width: 100%;
|
||||||
object-fit: contain !important;
|
height: 100%;
|
||||||
}
|
object-fit: contain;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
/* === 新增:PC端 隐藏进度条和时间 === */
|
||||||
|
|
||||||
|
/* 隐藏进度条 (滑动条) */
|
||||||
|
&::-webkit-media-controls-timeline {
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 隐藏当前播放时间 (例如 00:15) */
|
||||||
|
&::-webkit-media-controls-current-time-display {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏剩余时间 (例如 -02:30) */
|
||||||
|
&::-webkit-media-controls-time-remaining-display {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (可选) 如果你想连“回到直播”的按钮都隐藏,或者调整布局,可以加更多
|
||||||
|
但通常上面三个就够了,效果就是:播放/暂停 + 音量 + 画中画 + 全屏 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.stream-switcher {
|
.stream-switcher {
|
||||||
@@ -317,10 +368,22 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-tip {
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-tip {
|
||||||
|
margin-top: 8px;
|
||||||
|
text-align: center;
|
||||||
|
color: #d9534f;
|
||||||
|
}
|
||||||
|
|
||||||
.back-button {
|
.back-button {
|
||||||
display: block;
|
display: block;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
margin: 30px auto 0;
|
margin: 10px auto 0;
|
||||||
padding: 12px 24px;
|
padding: 12px 24px;
|
||||||
background: #3498db;
|
background: #3498db;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -337,7 +400,6 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.game-header {
|
.game-header {
|
||||||
// flex-direction: column;
|
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
.team-info {
|
.team-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -353,12 +415,6 @@ onBeforeUnmount(() => {
|
|||||||
.team-details {
|
.team-details {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
|
|||||||
940
src/components/NBASchedule - 副本.vue
Normal file
940
src/components/NBASchedule - 副本.vue
Normal file
@@ -0,0 +1,940 @@
|
|||||||
|
<template>
|
||||||
|
<div class="nba-schedule-container">
|
||||||
|
<div v-if="scheduleData?.data?.sponsor" class="sponsor-banner">
|
||||||
|
<h3>所有内容均来源互联网,反馈联系邮箱:super@2026123.xyz</h3>
|
||||||
|
<h4>遇到播放问题可以尝试切换浏览器、刷新重试</h4>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="ad">
|
||||||
|
<a href="https://1.aysj.xyz/#/register" target="_blank">
|
||||||
|
遨游世界加速器,畅游Google、YouTube、ChatGpt、ins等海外网站,支持多平台使用,仅需15/月
|
||||||
|
</a>
|
||||||
|
</div> -->
|
||||||
|
<div class="top">
|
||||||
|
<div class="login" v-if="!isLoggedIn">
|
||||||
|
<button class="login-btn" @click="openAuth">登录/注册</button>
|
||||||
|
</div>
|
||||||
|
<div class="user-info" v-else>
|
||||||
|
<span class="user-email">{{ userEmail }}</span>
|
||||||
|
<button class="logout-btn" @click="logout">退出</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="date-navigation">
|
||||||
|
<button
|
||||||
|
@click="changeDate('pre')"
|
||||||
|
class="nav-button"
|
||||||
|
:disabled="!scheduleData?.data?.preDate"
|
||||||
|
>
|
||||||
|
<span class="arrow">←</span>
|
||||||
|
{{ scheduleData?.data?.preDate || "无更早日期" }}
|
||||||
|
</button>
|
||||||
|
<div class="current-date">
|
||||||
|
{{ currentDisplayDate }}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="changeDate('next')"
|
||||||
|
class="nav-button"
|
||||||
|
:disabled="!scheduleData?.data?.nextDate"
|
||||||
|
>
|
||||||
|
{{ scheduleData?.data?.nextDate || "无更晚日期" }}
|
||||||
|
<span class="arrow">→</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="games-list">
|
||||||
|
<div v-for="group in scheduleData?.data?.groups" :key="group.date">
|
||||||
|
<div v-if="group.games && group.games.length > 0">
|
||||||
|
<div v-for="game in group.games" :key="game.gameId" class="game-card">
|
||||||
|
<div class="game-time" :class="getStatusClass(game.status)">
|
||||||
|
美国时间:{{ formatGameTime(game.dateTimeUtc) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="game-main">
|
||||||
|
<div
|
||||||
|
class="team home-team"
|
||||||
|
:class="{
|
||||||
|
'tbd-team': !game.teamValid,
|
||||||
|
winner: isWinner(game, 'home'),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="game.homeTeamLogoDark"
|
||||||
|
:alt="game.homeTeamName"
|
||||||
|
class="team-logo"
|
||||||
|
/>
|
||||||
|
<div class="team-info">
|
||||||
|
<div class="team-name">
|
||||||
|
<span class="city">{{ game.homeTeamCity }}</span>
|
||||||
|
<span class="name">{{ game.homeTeamName || "待定" }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="team-record">
|
||||||
|
<span v-if="game.homeTeamWins !== undefined">
|
||||||
|
{{ game.homeTeamWins }}胜{{ game.homeTeamLosses }}负
|
||||||
|
</span>
|
||||||
|
<span v-if="game.hasTotalWins" class="series-wins">
|
||||||
|
(系列赛{{ game.homeTeamTotalWins }}-{{
|
||||||
|
game.awayTeamTotalWins
|
||||||
|
}})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h4>主场</h4>
|
||||||
|
<div v-if="game.status !== 1" class="team-score">
|
||||||
|
{{ game.homeTeamScore }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="game-status">
|
||||||
|
<div v-if="game.status === 1" class="game-not-started">
|
||||||
|
北京时间:{{ game.startTime }} 开赛
|
||||||
|
</div>
|
||||||
|
<div v-else class="game-in-progress">
|
||||||
|
<div class="status-text">{{ game.statusText }}</div>
|
||||||
|
<div v-if="game.periodText" class="period-text">
|
||||||
|
{{ game.periodText }}
|
||||||
|
</div>
|
||||||
|
<div v-if="game.gameClock" class="game-clock">
|
||||||
|
{{ game.gameClock }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="team away-team"
|
||||||
|
:class="{
|
||||||
|
'tbd-team': !game.teamValid,
|
||||||
|
winner: isWinner(game, 'away'),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="game.awayTeamLogoDark"
|
||||||
|
:alt="game.awayTeamName"
|
||||||
|
class="team-logo"
|
||||||
|
/>
|
||||||
|
<div class="team-info">
|
||||||
|
<div class="team-name">
|
||||||
|
<span class="city">{{ game.awayTeamCity }}</span>
|
||||||
|
<span class="name">{{ game.awayTeamName || "待定" }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="team-record">
|
||||||
|
<span v-if="game.awayTeamWins !== undefined">
|
||||||
|
{{ game.awayTeamWins }}胜{{ game.awayTeamLosses }}负
|
||||||
|
</span>
|
||||||
|
<span v-if="game.hasTotalWins" class="series-wins">
|
||||||
|
(系列赛{{ game.awayTeamTotalWins }}-{{
|
||||||
|
game.homeTeamTotalWins
|
||||||
|
}})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h4>客场</h4>
|
||||||
|
<div v-if="game.status !== 1" class="team-score">
|
||||||
|
{{ game.awayTeamScore }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="live-buttons">
|
||||||
|
<!-- 当天的比赛 -->
|
||||||
|
<template v-if="isTodayGame(game)">
|
||||||
|
<!-- 当天且有 urls:显示直播按钮 -->
|
||||||
|
<!-- <template v-if="hasLiveStreams(game.gameId) || game.status == 3">
|
||||||
|
<button
|
||||||
|
v-for="stream in getLiveStreams(game.gameId)"
|
||||||
|
:key="stream.type"
|
||||||
|
@click="goToLive(game, stream)"
|
||||||
|
class="live-btn"
|
||||||
|
>
|
||||||
|
<span class="btn-icon">📺</span>
|
||||||
|
{{ getStreamName(stream.type) }}
|
||||||
|
</button>
|
||||||
|
</template> -->
|
||||||
|
|
||||||
|
<template v-if="hasLiveStreams(game.gameId) && game.status !== 3">
|
||||||
|
<button
|
||||||
|
v-for="stream in getLiveStreams(game.gameId)"
|
||||||
|
:key="stream.type"
|
||||||
|
@click="goToLive(game, stream)"
|
||||||
|
class="live-btn"
|
||||||
|
>
|
||||||
|
<span class="btn-icon">📺</span>
|
||||||
|
{{ getStreamName(stream.type) }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<div v-else-if="game.status === 3" class="no-live">
|
||||||
|
比赛已结束
|
||||||
|
</div>
|
||||||
|
<!-- 当天但暂时没有 urls -->
|
||||||
|
<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 subscribe-btn"
|
||||||
|
@click="handleSubscribe(game)"
|
||||||
|
>
|
||||||
|
订阅
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="game-footer">
|
||||||
|
<div class="game-arena">
|
||||||
|
<span v-if="game.arenaName">{{ game.arenaName }}</span>
|
||||||
|
<span v-else>场地待定</span>
|
||||||
|
</div>
|
||||||
|
<div class="game-season">
|
||||||
|
{{ game.seasonName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-games-message">
|
||||||
|
{{ currentDisplayDate }} 当天没有NBA比赛
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AuthModal
|
||||||
|
v-if="showAuth"
|
||||||
|
v-model="showAuth"
|
||||||
|
:default-mode="authMode"
|
||||||
|
@submit="handleAuthSubmit"
|
||||||
|
@send-code="handleSendCode"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { urls, fetchLiveUrl } from "@/api/nba";
|
||||||
|
import { sendRegisterCode } from "@/api/userApi";
|
||||||
|
import { useGameStore } from "@/stores/game";
|
||||||
|
import AuthModal from "@/components/AuthModal.vue";
|
||||||
|
import { computed, ref, onMounted } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useUserStore } from "@/stores/user";
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const { isLoggedIn, user } = storeToRefs(userStore);
|
||||||
|
|
||||||
|
const userEmail = computed(() => user.value || "已登录");
|
||||||
|
|
||||||
|
const showAuth = ref(false);
|
||||||
|
const authMode = ref("login");
|
||||||
|
const gameStore = useGameStore();
|
||||||
|
const router = useRouter();
|
||||||
|
const urlsData = ref([]);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const response = await urls();
|
||||||
|
urlsData.value = Array.isArray(response) ? response : [];
|
||||||
|
// console.log(urlsData.value);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("获取直播URL失败:", err);
|
||||||
|
urlsData.value = [];
|
||||||
|
}
|
||||||
|
// 2. 再检查当前登录用户(如果 session 还在,会自动登录)
|
||||||
|
if (!userStore.checkedLogin) {
|
||||||
|
await userStore.fetchCurrentUser(); // 调 /user/me,看 session 里有没有登录
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const isTodayGame = (game) => {
|
||||||
|
if (!game || !game.startDate) return false;
|
||||||
|
|
||||||
|
const gameDate = new Date(game.startDate);
|
||||||
|
if (isNaN(gameDate.getTime())) return false;
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
return (
|
||||||
|
gameDate.getFullYear() === today.getFullYear() &&
|
||||||
|
gameDate.getMonth() === today.getMonth() &&
|
||||||
|
gameDate.getDate() === today.getDate()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeGameId = (id) => {
|
||||||
|
if (id == null) return "";
|
||||||
|
// 转成字符串,左侧补 0 到 10 位,跟 /api/urls 里的 "0022500255" 对齐
|
||||||
|
return String(id).padStart(10, "0");
|
||||||
|
};
|
||||||
|
const hasLiveStreams = (gameId) => {
|
||||||
|
if (!urlsData.value || !gameId) return false;
|
||||||
|
const id = normalizeGameId(gameId);
|
||||||
|
|
||||||
|
for (const streamGroup of urlsData.value) {
|
||||||
|
if (streamGroup[id]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLiveStreams = (gameId) => {
|
||||||
|
const id = normalizeGameId(gameId);
|
||||||
|
for (const streamGroup of urlsData.value) {
|
||||||
|
if (streamGroup[id]) {
|
||||||
|
return streamGroup[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStreamName = (type) => {
|
||||||
|
const names = {
|
||||||
|
tx: "企鹅超清",
|
||||||
|
wl: "纬来体育",
|
||||||
|
nba: "高清原声",
|
||||||
|
mg: "咪咕高清",
|
||||||
|
zb: "高清直播",
|
||||||
|
};
|
||||||
|
return names[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAuth = () => {
|
||||||
|
showAuth.value = true;
|
||||||
|
authMode.value = "login";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAuthSubmit = async (payload) => {
|
||||||
|
if (payload.mode === "register") {
|
||||||
|
try {
|
||||||
|
const msg = await userStore.register({
|
||||||
|
email: payload.email,
|
||||||
|
password: payload.password,
|
||||||
|
code: payload.code,
|
||||||
|
});
|
||||||
|
alert(msg || "注册成功");
|
||||||
|
showAuth.value = false;
|
||||||
|
} catch (e) {
|
||||||
|
alert("注册失败,服务出现异常联系管理员");
|
||||||
|
}
|
||||||
|
} else if (payload.mode === "reset") {
|
||||||
|
try {
|
||||||
|
const msg = await userStore.changePassword({
|
||||||
|
email: payload.email,
|
||||||
|
password: payload.password,
|
||||||
|
code: payload.code,
|
||||||
|
});
|
||||||
|
alert(msg || "密码修改成功");
|
||||||
|
showAuth.value = false;
|
||||||
|
} catch (e) {
|
||||||
|
alert(e.message || "修改密码失败,服务出现异常联系管理员");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await userStore.login({
|
||||||
|
email: payload.email,
|
||||||
|
password: payload.password,
|
||||||
|
});
|
||||||
|
alert("登录成功");
|
||||||
|
showAuth.value = false;
|
||||||
|
} catch (e) {
|
||||||
|
alert(e.message || "登录失败,服务出现异常联系管理员");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendCode = ({ email, mode }) => {
|
||||||
|
const type = mode === "reset" ? 2 : 1;
|
||||||
|
sendRegisterCode(email, type)
|
||||||
|
.then((res) => {
|
||||||
|
alert(res)
|
||||||
|
// alert("验证码已发送,因邮件有延迟 请不要频繁发送");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
alert("服务出现异常,请联系管理员");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
try {
|
||||||
|
await userStore.logout(); // 调 /user/logout + 清空 user
|
||||||
|
alert("已退出登录");
|
||||||
|
} catch (e) {
|
||||||
|
alert("退出登录异常,但本地已清空登录状态");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubscribe = (game) => {
|
||||||
|
if (!isLoggedIn.value) {
|
||||||
|
alert("订阅功能需登录使用");
|
||||||
|
// openAuth(); //(可选)如果你想自动弹出登录弹窗
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录了
|
||||||
|
alert("订阅功能等待开放");
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 未来真正开放订阅后,在这里写接口逻辑:
|
||||||
|
// console.log("订阅比赛:", game.gameId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToLive = async (game, stream) => {
|
||||||
|
if (!game?.gameId || !stream?.type) return;
|
||||||
|
// 未登录用户提示请先登录
|
||||||
|
if (!isLoggedIn.value) {
|
||||||
|
alert("请先登录后再观看直播");
|
||||||
|
openAuth();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const liveResp = await fetchLiveUrl(game.gameId, stream.type);
|
||||||
|
const url =
|
||||||
|
liveResp?.url ||
|
||||||
|
liveResp?.data?.url ||
|
||||||
|
liveResp?.data?.data ||
|
||||||
|
(typeof liveResp === "string" ? liveResp : null);
|
||||||
|
gameStore.setCurrentGame({
|
||||||
|
gameData,
|
||||||
|
gameId: game.gameId,
|
||||||
|
currentStream: { ...stream, url },
|
||||||
|
allStreams: getLiveStreams(game.gameId),
|
||||||
|
});
|
||||||
|
router.push({
|
||||||
|
name: "Play",
|
||||||
|
params: { gameId: game.gameId },
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("获取直播地址失败:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
scheduleData: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["dateChange"]);
|
||||||
|
|
||||||
|
const currentDisplayDate = computed(() => {
|
||||||
|
if (!props.scheduleData?.data?.start) return "加载中...";
|
||||||
|
const dateStr = props.scheduleData.data.start;
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const weekdays = [
|
||||||
|
"星期日",
|
||||||
|
"星期一",
|
||||||
|
"星期二",
|
||||||
|
"星期三",
|
||||||
|
"星期四",
|
||||||
|
"星期五",
|
||||||
|
"星期六",
|
||||||
|
];
|
||||||
|
const weekday = weekdays[date.getDay()];
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||||
|
const day = date.getDate().toString().padStart(2, "0");
|
||||||
|
|
||||||
|
return `${year}年${month}月${day}日 ${weekday}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatGameTime = (utcTime) => {
|
||||||
|
if (!utcTime) return "时间待定";
|
||||||
|
const date = new Date(utcTime);
|
||||||
|
const hours = date.getHours().toString().padStart(2, "0");
|
||||||
|
const minutes = date.getMinutes().toString().padStart(2, "0");
|
||||||
|
return `${hours}:${minutes}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusClass = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 1:
|
||||||
|
return "not-started";
|
||||||
|
case 2:
|
||||||
|
return "in-progress";
|
||||||
|
case 3:
|
||||||
|
return "finished";
|
||||||
|
default:
|
||||||
|
return "not-started";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeDate = (direction) => {
|
||||||
|
if (!props.scheduleData?.data) return;
|
||||||
|
const date =
|
||||||
|
direction === "pre"
|
||||||
|
? props.scheduleData.data.preDate
|
||||||
|
: props.scheduleData.data.nextDate;
|
||||||
|
if (date) {
|
||||||
|
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>
|
||||||
|
.subscribe-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-btn:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nba-schedule-container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 10px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
|
"Helvetica Neue", Arial, sans-serif;
|
||||||
|
}
|
||||||
|
.no-live {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #808080;
|
||||||
|
color: #ffffff;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sponsor-banner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
.sponsor-banner h3,
|
||||||
|
.sponsor-banner h4 {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-navigation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-date {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1b1f27;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button:hover:not(:disabled) {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.games-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-card {
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-time {
|
||||||
|
padding: 12px 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-started {
|
||||||
|
background-color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.in-progress {
|
||||||
|
background-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finished {
|
||||||
|
background-color: #3dbe5b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-main {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-logo {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
margin-right: 20px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-name {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-name .city {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-name .name {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 20px;
|
||||||
|
margin: 4px 0;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-record {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #868e96;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-score {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
min-width: 60px;
|
||||||
|
text-align: center;
|
||||||
|
margin-left: 20px;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbd-team {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-status {
|
||||||
|
padding: 12px 0;
|
||||||
|
text-align: center;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-top: 1px dashed #e9ecef;
|
||||||
|
border-bottom: 1px dashed #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-not-started {
|
||||||
|
color: #0008ff;
|
||||||
|
font-size: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-in-progress {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #dc3545;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.period-text,
|
||||||
|
.game-clock {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-team {
|
||||||
|
border-top: 1px solid #f1f3f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 20px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-arena {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-season {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d428a;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.game-card {
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team {
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-logo {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-name .name {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-score {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.date-navigation {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-date {
|
||||||
|
order: -1;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-logo {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-name .name {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-score {
|
||||||
|
font-size: 20px;
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-top: 1px solid #f1f3f5;
|
||||||
|
border-bottom: 1px solid #f1f3f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-btn {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-btn.primary {
|
||||||
|
background-color: #1d428a;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-btn.primary:hover {
|
||||||
|
background-color: #15316e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-btn.secondary {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #1d428a;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-btn.secondary:hover {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.live-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-btn {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-games-message {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #666;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team.winner {
|
||||||
|
background-color: rgba(183, 239, 167, 0.898);
|
||||||
|
border-left: 3px solid #a4b1ee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team.winner .team-name {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #d4e66a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team.winner .team-score {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #68db81;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
display: flex;
|
||||||
|
padding: 5px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border: 1px solid #1d428a;
|
||||||
|
background: #1d428a;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-email {
|
||||||
|
color: #1d428a;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid #e74c3c;
|
||||||
|
background: #e74c3c;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #4d58f5;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.ad{
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #d4eef1;
|
||||||
|
/* border: 1px solid #79d0e4; */
|
||||||
|
border-radius: 6px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,16 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<!-- <div class="zfb" v-if="showZfb">
|
||||||
|
<img src="../assets/imgs/qw.jpg" alt="千问有礼">
|
||||||
|
<div class="zfb-text">下载千问app,扫码免费点奶茶</div>
|
||||||
|
<button class="close-btn" @click="closeZfb">关闭</button>
|
||||||
|
</div> -->
|
||||||
<div class="nba-schedule-container">
|
<div class="nba-schedule-container">
|
||||||
<!-- 赞助商信息 -->
|
<div class="sponsor-banner">
|
||||||
<div v-if="scheduleData?.data?.sponsor" class="sponsor-banner">
|
<h3>所有内容均来源互联网,反馈邮箱:super@2026123.xyz</h3>
|
||||||
<span>所有内容均来源互联网,有问题请联系邮箱:xdd9@vip.qq.com</span>
|
<h4>遇到播放问题可以尝试切换浏览器、刷新重试</h4>
|
||||||
<!-- <img
|
<h4>推荐使用谷歌浏览器</h4>
|
||||||
:src="scheduleData.data.sponsor.logo"
|
</div>
|
||||||
:alt="scheduleData.data.sponsor.name"
|
<div class="ad">
|
||||||
class="sponsor-logo"
|
<a href="https://1.aysj.xyz/#/register" target="_blank">
|
||||||
/> -->
|
遨游世界VPN,畅游YouTube、ChatGpt、ins、telegram等海外应用,支持苹果、安卓、电脑使用,仅需20/月,点击试用
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="top">
|
||||||
|
<div class="login" v-if="!isLoggedIn">
|
||||||
|
<button class="login-btn" @click="openAuth">登录/注册</button>
|
||||||
|
</div>
|
||||||
|
<div class="user-info" v-else>
|
||||||
|
<span class="user-email">{{ userEmail }}</span>
|
||||||
|
<button class="logout-btn" @click="logout">退出</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 赛程日期导航 -->
|
|
||||||
<div class="date-navigation">
|
<div class="date-navigation">
|
||||||
<button
|
<button
|
||||||
@click="changeDate('pre')"
|
@click="changeDate('pre')"
|
||||||
@@ -33,70 +47,20 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 比赛列表 - 每行一场比赛 -->
|
|
||||||
<div class="games-list">
|
<div class="games-list">
|
||||||
<div v-for="group in scheduleData?.data?.groups" :key="group.date">
|
<div v-for="group in scheduleData?.data?.groups" :key="group.date">
|
||||||
<!-- 添加判断:当games数组为空时显示提示信息 -->
|
|
||||||
<div v-if="group.games && group.games.length > 0">
|
<div v-if="group.games && group.games.length > 0">
|
||||||
<div v-for="game in group.games" :key="game.gameId" class="game-card">
|
<div v-for="game in group.games" :key="game.gameId" class="game-card">
|
||||||
<!-- 比赛时间 -->
|
|
||||||
<div class="game-time" :class="getStatusClass(game.status)">
|
<div class="game-time" :class="getStatusClass(game.status)">
|
||||||
美国时间:{{ formatGameTime(game.dateTimeUtc) }}
|
美国时间:{{ formatGameTime(game.dateTimeUtc) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 比赛主要内容 -->
|
|
||||||
<div class="game-main">
|
<div class="game-main">
|
||||||
<!-- 客队信息 -->
|
|
||||||
<div
|
|
||||||
class="team away-team"
|
|
||||||
:class="{
|
|
||||||
'tbd-team': !game.teamValid,
|
|
||||||
winner: isWinner(game, 'away'), // 添加判断是否为胜者
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
:src="game.awayTeamLogoDark"
|
|
||||||
:alt="game.awayTeamName"
|
|
||||||
class="team-logo"
|
|
||||||
/>
|
|
||||||
<div class="team-info">
|
|
||||||
<div class="team-name">
|
|
||||||
<span class="city">{{ game.awayTeamCity }}</span>
|
|
||||||
<span class="name">{{ game.awayTeamName || "待定" }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="team-record">
|
|
||||||
<span v-if="game.awayTeamWins !== undefined">
|
|
||||||
{{ game.awayTeamWins }}胜-{{ game.awayTeamLosses }}负
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="game.status !== 1" class="team-score">
|
|
||||||
{{ game.awayTeamScore }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 比赛状态 -->
|
|
||||||
<div class="game-status">
|
|
||||||
<div v-if="game.status === 1" class="game-not-started">
|
|
||||||
北京时间:{{ game.startTime }} 开始
|
|
||||||
</div>
|
|
||||||
<div v-else class="game-in-progress">
|
|
||||||
<div class="status-text">{{ game.statusText }}</div>
|
|
||||||
<div v-if="game.periodText" class="period-text">
|
|
||||||
{{ game.periodText }}
|
|
||||||
</div>
|
|
||||||
<div v-if="game.gameClock" class="game-clock">
|
|
||||||
{{ game.gameClock }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 主队信息 -->
|
|
||||||
<div
|
<div
|
||||||
class="team home-team"
|
class="team home-team"
|
||||||
:class="{
|
:class="{
|
||||||
'tbd-team': !game.teamValid,
|
'tbd-team': !game.teamValid,
|
||||||
winner: isWinner(game, 'home'), // 添加判断是否为胜者
|
winner: isWinner(game, 'home'),
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -111,48 +75,118 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="team-record">
|
<div class="team-record">
|
||||||
<span v-if="game.homeTeamWins !== undefined">
|
<span v-if="game.homeTeamWins !== undefined">
|
||||||
{{ game.homeTeamWins }}胜-{{ game.homeTeamLosses }}负
|
{{ game.homeTeamWins }}胜{{ game.homeTeamLosses }}负
|
||||||
|
</span>
|
||||||
|
<span v-if="game.hasTotalWins" class="series-wins">
|
||||||
|
(系列赛{{ game.homeTeamTotalWins }}-{{
|
||||||
|
game.awayTeamTotalWins
|
||||||
|
}})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<h4>主场</h4>
|
||||||
<div v-if="game.status !== 1" class="team-score">
|
<div v-if="game.status !== 1" class="team-score">
|
||||||
{{ game.homeTeamScore }}
|
{{ game.homeTeamScore }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div class="game-status">
|
||||||
|
<div v-if="game.status === 1" class="game-not-started">
|
||||||
|
北京时间:{{ game.startTime }} 开赛
|
||||||
|
</div>
|
||||||
|
<div v-else class="game-in-progress">
|
||||||
|
<div class="status-text">{{ game.statusText }}</div>
|
||||||
|
<div v-if="game.periodText" class="period-text">
|
||||||
|
{{ game.periodText }}
|
||||||
|
</div>
|
||||||
|
<div v-if="game.gameClock" class="game-clock">
|
||||||
|
{{ game.gameClock }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="team away-team"
|
||||||
|
:class="{
|
||||||
|
'tbd-team': !game.teamValid,
|
||||||
|
winner: isWinner(game, 'away'),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="game.awayTeamLogoDark"
|
||||||
|
:alt="game.awayTeamName"
|
||||||
|
class="team-logo"
|
||||||
|
/>
|
||||||
|
<div class="team-info">
|
||||||
|
<div class="team-name">
|
||||||
|
<span class="city">{{ game.awayTeamCity }}</span>
|
||||||
|
<span class="name">{{ game.awayTeamName || "待定" }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="team-record">
|
||||||
|
<span v-if="game.awayTeamWins !== undefined">
|
||||||
|
{{ game.awayTeamWins }}胜{{ game.awayTeamLosses }}负
|
||||||
|
</span>
|
||||||
|
<span v-if="game.hasTotalWins" class="series-wins">
|
||||||
|
(系列赛{{ game.awayTeamTotalWins }}-{{
|
||||||
|
game.homeTeamTotalWins
|
||||||
|
}})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h4>客场</h4>
|
||||||
|
<div v-if="game.status !== 1" class="team-score">
|
||||||
|
{{ game.awayTeamScore }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="live-buttons">
|
<div class="live-buttons">
|
||||||
<!-- 进行中的比赛 -->
|
<!-- 当天的比赛 -->
|
||||||
<template v-if="game.status === 2">
|
<template v-if="isTodayGame(game)">
|
||||||
<!-- 只对当天比赛显示直播按钮 -->
|
<!-- 当天且有 urls:显示直播按钮 -->
|
||||||
<template v-if="isTodayGame(game)">
|
<!-- <template v-if="hasLiveStreams(game.gameId) || game.status == 3">
|
||||||
<template v-if="hasLiveStreams(game.gameId)">
|
<button
|
||||||
<button
|
v-for="stream in getLiveStreams(game.gameId)"
|
||||||
v-for="stream in getLiveStreams(game.gameId)"
|
:key="stream.type"
|
||||||
:key="stream.type"
|
@click="goToLive(game, stream)"
|
||||||
@click="goToLive(game, stream)"
|
class="live-btn"
|
||||||
class="live-btn"
|
>
|
||||||
>
|
<span class="btn-icon">📺</span>
|
||||||
<span class="btn-icon">📺</span>
|
{{ getStreamName(stream.type) }}
|
||||||
{{ getStreamName(stream.type) }}
|
</button>
|
||||||
</button>
|
</template> -->
|
||||||
</template>
|
|
||||||
<div v-else class="no-live">无直播信号</div>
|
<template v-if="hasLiveStreams(game.gameId) && game.status !== 3">
|
||||||
|
<button
|
||||||
|
v-for="stream in getLiveStreams(game.gameId)"
|
||||||
|
:key="stream.type"
|
||||||
|
@click="goToLive(game, stream)"
|
||||||
|
class="live-btn"
|
||||||
|
>
|
||||||
|
<span class="btn-icon">📺</span>
|
||||||
|
{{ getStreamName(stream.type) }}
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<!-- 非当天进行中比赛(理论上不应该存在) -->
|
<div v-else-if="game.status === 3" class="no-live">
|
||||||
<div v-else class="no-live">比赛进行中</div>
|
比赛已结束
|
||||||
|
</div>
|
||||||
|
<!-- 当天但暂时没有 urls -->
|
||||||
|
<div v-else class="no-live">无直播信息</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 已结束的比赛(无论是否当天) -->
|
<!-- 不是当天:已结束 -->
|
||||||
<div v-else-if="game.status === 3" class="no-live">
|
<div v-else-if="game.status === 3" class="no-live">
|
||||||
比赛已结束
|
比赛已结束
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 不是当天:未结束,显示订阅 -->
|
||||||
<!-- 未开始的比赛 -->
|
<div
|
||||||
<div v-else class="no-live">未开始</div>
|
v-else
|
||||||
|
class="no-live subscribe-btn"
|
||||||
|
@click="handleSubscribe(game)"
|
||||||
|
>
|
||||||
|
订阅
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 比赛场地和赛季信息 -->
|
|
||||||
<div class="game-footer">
|
<div class="game-footer">
|
||||||
<div class="game-arena">
|
<div class="game-arena">
|
||||||
<span v-if="game.arenaName">{{ game.arenaName }}</span>
|
<span v-if="game.arenaName">{{ game.arenaName }}</span>
|
||||||
@@ -164,96 +198,211 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 当没有比赛时显示提示信息 -->
|
|
||||||
<div v-else class="no-games-message">
|
<div v-else class="no-games-message">
|
||||||
{{ currentDisplayDate }} 当天没有NBA比赛
|
{{ currentDisplayDate }} 当天没有NBA比赛
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<AuthModal
|
||||||
|
v-if="showAuth"
|
||||||
|
v-model="showAuth"
|
||||||
|
:default-mode="authMode"
|
||||||
|
@submit="handleAuthSubmit"
|
||||||
|
@send-code="handleSendCode"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from "vue";
|
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { urls } from "@/api/nba";
|
import { urls, fetchLiveUrl } from "@/api/nba";
|
||||||
import { onMounted } from "vue";
|
import { sendRegisterCode } from "@/api/userApi";
|
||||||
import { useGameStore } from "@/stores/game";
|
import { useGameStore } from "@/stores/game";
|
||||||
|
import AuthModal from "@/components/AuthModal.vue";
|
||||||
|
import { computed, ref, onMounted } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useUserStore } from "@/stores/user";
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const { isLoggedIn, user } = storeToRefs(userStore);
|
||||||
|
|
||||||
|
const userEmail = computed(() => user.value || "已登录");
|
||||||
|
|
||||||
|
const showAuth = ref(false);
|
||||||
|
const authMode = ref("login");
|
||||||
const gameStore = useGameStore();
|
const gameStore = useGameStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const urlsData = ref([]);
|
const urlsData = ref([]);
|
||||||
|
const showZfb = ref(true);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await urls();
|
const response = await urls();
|
||||||
urlsData.value = response || [];
|
urlsData.value = Array.isArray(response) ? response : [];
|
||||||
|
// console.log(urlsData.value);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("获取直播URL失败:", err);
|
console.error("获取直播URL失败:", err);
|
||||||
urlsData.value = [];
|
urlsData.value = [];
|
||||||
}
|
}
|
||||||
|
// 2. 再检查当前登录用户(如果 session 还在,会自动登录)
|
||||||
|
if (!userStore.checkedLogin) {
|
||||||
|
await userStore.fetchCurrentUser(); // 调 /user/me,看 session 里有没有登录
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 判断是否为当天比赛(无论状态如何)
|
|
||||||
const isTodayGame = (game) => {
|
const isTodayGame = (game) => {
|
||||||
// 获取今天的日期(北京时间)
|
if (!game || !game.startDate) return false;
|
||||||
const today = new Date();
|
|
||||||
const todayStr = `${today.getFullYear()}-${(today.getMonth() + 1)
|
|
||||||
.toString()
|
|
||||||
.padStart(2, "0")}-${today.getDate().toString().padStart(2, "0")}`;
|
|
||||||
|
|
||||||
// 直接比较 startDate(已经是北京时间)
|
const gameDate = new Date(game.startDate);
|
||||||
return game.startDate === todayStr;
|
if (isNaN(gameDate.getTime())) return false;
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
return (
|
||||||
|
gameDate.getFullYear() === today.getFullYear() &&
|
||||||
|
gameDate.getMonth() === today.getMonth() &&
|
||||||
|
gameDate.getDate() === today.getDate()
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检查比赛是否有直播流
|
const normalizeGameId = (id) => {
|
||||||
|
if (id == null) return "";
|
||||||
|
// 转成字符串,左侧补 0 到 10 位,跟 /api/urls 里的 "0022500255" 对齐
|
||||||
|
return String(id).padStart(10, "0");
|
||||||
|
};
|
||||||
const hasLiveStreams = (gameId) => {
|
const hasLiveStreams = (gameId) => {
|
||||||
if (!urlsData.value || !gameId) return false;
|
if (!urlsData.value || !gameId) return false;
|
||||||
|
const id = normalizeGameId(gameId);
|
||||||
|
|
||||||
// 遍历所有直播流数据
|
|
||||||
for (const streamGroup of urlsData.value) {
|
for (const streamGroup of urlsData.value) {
|
||||||
if (streamGroup[gameId]) {
|
if (streamGroup[id]) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取比赛的直播流
|
|
||||||
const getLiveStreams = (gameId) => {
|
const getLiveStreams = (gameId) => {
|
||||||
const id = String(gameId); // 转为字符串
|
const id = normalizeGameId(gameId);
|
||||||
for (const streamGroup of urlsData.value) {
|
for (const streamGroup of urlsData.value) {
|
||||||
if (streamGroup[id]) return streamGroup[id];
|
if (streamGroup[id]) {
|
||||||
|
return streamGroup[id];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取流名称
|
|
||||||
const getStreamName = (type) => {
|
const getStreamName = (type) => {
|
||||||
const names = {
|
const names = {
|
||||||
tx: "企鹅体育",
|
tx: "企鹅超清",
|
||||||
wl: "纬来体育",
|
wl: "纬来体育",
|
||||||
nba: "高清原声",
|
nba: "高清原声",
|
||||||
mg: "咪咕体育",
|
mg: "咪咕高清",
|
||||||
zb: "高清直播",
|
zb: "高清直播",
|
||||||
};
|
};
|
||||||
return names[type] || type;
|
return names[type] || type;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 跳转到直播页面
|
const openAuth = () => {
|
||||||
const goToLive = (game, stream) => {
|
showAuth.value = true;
|
||||||
// 准备比赛数据
|
authMode.value = "login";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAuthSubmit = async (payload) => {
|
||||||
|
if (payload.mode === "register") {
|
||||||
|
try {
|
||||||
|
const msg = await userStore.register({
|
||||||
|
email: payload.email,
|
||||||
|
password: payload.password,
|
||||||
|
code: payload.code,
|
||||||
|
});
|
||||||
|
alert(msg || "注册成功");
|
||||||
|
showAuth.value = false;
|
||||||
|
} catch (e) {
|
||||||
|
alert("注册失败,服务出现异常联系管理员");
|
||||||
|
}
|
||||||
|
} else if (payload.mode === "reset") {
|
||||||
|
try {
|
||||||
|
const msg = await userStore.changePassword({
|
||||||
|
email: payload.email,
|
||||||
|
password: payload.password,
|
||||||
|
code: payload.code,
|
||||||
|
});
|
||||||
|
alert(msg || "密码修改成功");
|
||||||
|
showAuth.value = false;
|
||||||
|
} catch (e) {
|
||||||
|
alert(e.message || "修改密码失败,服务出现异常联系管理员");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await userStore.login({
|
||||||
|
email: payload.email,
|
||||||
|
password: payload.password,
|
||||||
|
});
|
||||||
|
alert("登录成功");
|
||||||
|
showAuth.value = false;
|
||||||
|
} catch (e) {
|
||||||
|
alert(e.message || "登录失败,服务出现异常联系管理员");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendCode = ({ email, mode }) => {
|
||||||
|
const type = mode === "reset" ? 2 : 1;
|
||||||
|
sendRegisterCode(email, type)
|
||||||
|
.then((res) => {
|
||||||
|
alert(res)
|
||||||
|
// alert("验证码已发送,因邮件有延迟 请不要频繁发送");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
alert("服务出现异常,请联系管理员");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
try {
|
||||||
|
await userStore.logout(); // 调 /user/logout + 清空 user
|
||||||
|
alert("已退出登录");
|
||||||
|
} catch (e) {
|
||||||
|
alert("退出登录异常,但本地已清空登录状态");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubscribe = (game) => {
|
||||||
|
if (!isLoggedIn.value) {
|
||||||
|
alert("订阅功能需登录使用");
|
||||||
|
// openAuth(); //(可选)如果你想自动弹出登录弹窗
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录了
|
||||||
|
alert("订阅功能等待开放");
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 未来真正开放订阅后,在这里写接口逻辑:
|
||||||
|
// console.log("订阅比赛:", game.gameId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToLive = async (game, stream) => {
|
||||||
|
if (!game?.gameId || !stream?.type) return;
|
||||||
|
// 未登录用户提示请先登录
|
||||||
|
if (!isLoggedIn.value) {
|
||||||
|
alert("请先登录后再观看直播");
|
||||||
|
openAuth();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const gameData = {
|
const gameData = {
|
||||||
homeTeam: {
|
homeTeam: {
|
||||||
name: game.homeTeamName,
|
name: game.homeTeamName,
|
||||||
logo: game.homeTeamLogoDark,
|
logo: game.homeTeamLogoDark,
|
||||||
city: game.homeTeamCity,
|
city: game.homeTeamCity,
|
||||||
record: `${game.homeTeamWins}胜-${game.homeTeamLosses}负`,
|
record: `${game.homeTeamWins}胜${game.homeTeamLosses}负`,
|
||||||
},
|
},
|
||||||
awayTeam: {
|
awayTeam: {
|
||||||
name: game.awayTeamName,
|
name: game.awayTeamName,
|
||||||
logo: game.awayTeamLogoDark,
|
logo: game.awayTeamLogoDark,
|
||||||
city: game.awayTeamCity,
|
city: game.awayTeamCity,
|
||||||
record: `${game.awayTeamWins}胜-${game.awayTeamLosses}负`,
|
record: `${game.awayTeamWins}胜${game.awayTeamLosses}负`,
|
||||||
},
|
},
|
||||||
gameInfo: {
|
gameInfo: {
|
||||||
arena: game.arenaName,
|
arena: game.arenaName,
|
||||||
@@ -261,20 +410,26 @@ const goToLive = (game, stream) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 存储到Pinia
|
try {
|
||||||
gameStore.setCurrentGame({
|
const liveResp = await fetchLiveUrl(game.gameId, stream.type);
|
||||||
gameData,
|
const url =
|
||||||
currentStream: stream,
|
liveResp?.url ||
|
||||||
allStreams: getLiveStreams(game.gameId),
|
liveResp?.data?.url ||
|
||||||
});
|
liveResp?.data?.data ||
|
||||||
|
(typeof liveResp === "string" ? liveResp : null);
|
||||||
// 导航到播放页
|
gameStore.setCurrentGame({
|
||||||
router.push({
|
gameData,
|
||||||
name: "Play",
|
|
||||||
params: {
|
|
||||||
gameId: game.gameId,
|
gameId: game.gameId,
|
||||||
},
|
currentStream: { ...stream, url },
|
||||||
});
|
allStreams: getLiveStreams(game.gameId),
|
||||||
|
});
|
||||||
|
router.push({
|
||||||
|
name: "Play",
|
||||||
|
params: { gameId: game.gameId },
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("获取直播地址失败:", err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -294,14 +449,10 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(["dateChange"]);
|
const emit = defineEmits(["dateChange"]);
|
||||||
|
|
||||||
// 当前显示日期(带星期几)
|
|
||||||
const currentDisplayDate = computed(() => {
|
const currentDisplayDate = computed(() => {
|
||||||
if (!props.scheduleData?.data?.start) return "加载中...";
|
if (!props.scheduleData?.data?.start) return "加载中...";
|
||||||
|
|
||||||
const dateStr = props.scheduleData.data.start;
|
const dateStr = props.scheduleData.data.start;
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateStr);
|
||||||
|
|
||||||
// 星期几的中文名称
|
|
||||||
const weekdays = [
|
const weekdays = [
|
||||||
"星期日",
|
"星期日",
|
||||||
"星期一",
|
"星期一",
|
||||||
@@ -312,8 +463,6 @@ const currentDisplayDate = computed(() => {
|
|||||||
"星期六",
|
"星期六",
|
||||||
];
|
];
|
||||||
const weekday = weekdays[date.getDay()];
|
const weekday = weekdays[date.getDay()];
|
||||||
|
|
||||||
// 格式化日期为 YYYY年MM月DD日
|
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||||
const day = date.getDate().toString().padStart(2, "0");
|
const day = date.getDate().toString().padStart(2, "0");
|
||||||
@@ -321,7 +470,6 @@ const currentDisplayDate = computed(() => {
|
|||||||
return `${year}年${month}月${day}日 ${weekday}`;
|
return `${year}年${month}月${day}日 ${weekday}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 格式化比赛时间
|
|
||||||
const formatGameTime = (utcTime) => {
|
const formatGameTime = (utcTime) => {
|
||||||
if (!utcTime) return "时间待定";
|
if (!utcTime) return "时间待定";
|
||||||
const date = new Date(utcTime);
|
const date = new Date(utcTime);
|
||||||
@@ -330,57 +478,60 @@ const formatGameTime = (utcTime) => {
|
|||||||
return `${hours}:${minutes}`;
|
return `${hours}:${minutes}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取比赛状态对应的样式类
|
|
||||||
const getStatusClass = (status) => {
|
const getStatusClass = (status) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 1:
|
case 1:
|
||||||
return "not-started"; // 未开始
|
return "not-started";
|
||||||
case 2:
|
case 2:
|
||||||
return "in-progress"; // 进行中
|
return "in-progress";
|
||||||
case 3:
|
case 3:
|
||||||
return "finished"; // 已结束
|
return "finished";
|
||||||
default:
|
default:
|
||||||
return "not-started";
|
return "not-started";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切换日期
|
|
||||||
const changeDate = (direction) => {
|
const changeDate = (direction) => {
|
||||||
if (!props.scheduleData?.data) return;
|
if (!props.scheduleData?.data) return;
|
||||||
|
|
||||||
const date =
|
const date =
|
||||||
direction === "pre"
|
direction === "pre"
|
||||||
? props.scheduleData.data.preDate
|
? props.scheduleData.data.preDate
|
||||||
: props.scheduleData.data.nextDate;
|
: props.scheduleData.data.nextDate;
|
||||||
|
|
||||||
if (date) {
|
if (date) {
|
||||||
emit("dateChange", date);
|
emit("dateChange", date);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 判断某支球队是否是胜者
|
|
||||||
const isWinner = (game, teamType) => {
|
const isWinner = (game, teamType) => {
|
||||||
// 如果比赛未结束,没有胜者
|
|
||||||
if (game.status !== 3) return false;
|
if (game.status !== 3) return false;
|
||||||
|
|
||||||
// 比较比分
|
|
||||||
if (teamType === "away") {
|
if (teamType === "away") {
|
||||||
return game.awayTeamScore > game.homeTeamScore;
|
return game.awayTeamScore > game.homeTeamScore;
|
||||||
} else {
|
} else {
|
||||||
return game.homeTeamScore > game.awayTeamScore;
|
return game.homeTeamScore > game.awayTeamScore;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeZfb = () => {
|
||||||
|
showZfb.value = false;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.subscribe-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscribe-btn:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
.nba-schedule-container {
|
.nba-schedule-container {
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px;
|
padding: 10px;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
"Helvetica Neue", Arial, sans-serif;
|
"Helvetica Neue", Arial, sans-serif;
|
||||||
}
|
}
|
||||||
/* 添加未开播样式居中显示 */
|
|
||||||
.no-live {
|
.no-live {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -393,40 +544,36 @@ const isWinner = (game, teamType) => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 赞助商样式 */
|
|
||||||
.sponsor-banner {
|
.sponsor-banner {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 10px;
|
||||||
padding: 12px;
|
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid #e9ecef;
|
border: 1px solid #e9ecef;
|
||||||
}
|
}
|
||||||
|
.sponsor-banner h3,
|
||||||
.sponsor-logo {
|
.sponsor-banner h4 {
|
||||||
height: 36px;
|
margin: 5px;
|
||||||
margin-left: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 日期导航样式 */
|
|
||||||
.date-navigation {
|
.date-navigation {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 10px;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 10px;
|
||||||
border-bottom: 1px solid #e9ecef;
|
border-bottom: 1px solid #e9ecef;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-date {
|
.current-date {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1d428a; /* NBA 蓝色 */
|
color: #1b1f27;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-button {
|
.nav-button {
|
||||||
@@ -453,14 +600,12 @@ const isWinner = (game, teamType) => {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 比赛列表 - 每行一场比赛 */
|
|
||||||
.games-list {
|
.games-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 25px;
|
gap: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 比赛卡片样式 - 大气风格 */
|
|
||||||
.game-card {
|
.game-card {
|
||||||
border: 1px solid #e9ecef;
|
border: 1px solid #e9ecef;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -474,7 +619,6 @@ const isWinner = (game, teamType) => {
|
|||||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 比赛时间 */
|
|
||||||
.game-time {
|
.game-time {
|
||||||
padding: 12px 20px;
|
padding: 12px 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -483,23 +627,21 @@ const isWinner = (game, teamType) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.not-started {
|
.not-started {
|
||||||
background-color: #6c757d; /* 灰色 - 未开始 */
|
background-color: #6c757d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.in-progress {
|
.in-progress {
|
||||||
background-color: #dc3545; /* 红色 - 进行中 */
|
background-color: #dc3545;
|
||||||
}
|
}
|
||||||
|
|
||||||
.finished {
|
.finished {
|
||||||
background-color: #28a745; /* 绿色 - 已结束 */
|
background-color: #3dbe5b;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 比赛主要内容 */
|
|
||||||
.game-main {
|
.game-main {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 球队样式 */
|
|
||||||
.team {
|
.team {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -551,12 +693,10 @@ const isWinner = (game, teamType) => {
|
|||||||
color: #212529;
|
color: #212529;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 待定球队样式 */
|
|
||||||
.tbd-team {
|
.tbd-team {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 比赛状态 */
|
|
||||||
.game-status {
|
.game-status {
|
||||||
padding: 12px 0;
|
padding: 12px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -566,8 +706,8 @@ const isWinner = (game, teamType) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.game-not-started {
|
.game-not-started {
|
||||||
color: #5a7cec;
|
color: #0008ff;
|
||||||
font-size: 18px;
|
font-size: 19px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-in-progress {
|
.game-in-progress {
|
||||||
@@ -588,12 +728,10 @@ const isWinner = (game, teamType) => {
|
|||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 主队样式 */
|
|
||||||
.home-team {
|
.home-team {
|
||||||
border-top: 1px solid #f1f3f5;
|
border-top: 1px solid #f1f3f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 比赛页脚 */
|
|
||||||
.game-footer {
|
.game-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -614,7 +752,6 @@ const isWinner = (game, teamType) => {
|
|||||||
color: #1d428a;
|
color: #1d428a;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.game-card {
|
.game-card {
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
@@ -672,10 +809,11 @@ const isWinner = (game, teamType) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 新增直播间按钮样式 */
|
|
||||||
.live-buttons {
|
.live-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
padding: 15px 20px;
|
padding: 15px 20px;
|
||||||
border-top: 1px solid #f1f3f5;
|
border-top: 1px solid #f1f3f5;
|
||||||
border-bottom: 1px solid #f1f3f5;
|
border-bottom: 1px solid #f1f3f5;
|
||||||
@@ -719,7 +857,6 @@ const isWinner = (game, teamType) => {
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式调整 */
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.live-buttons {
|
.live-buttons {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -730,6 +867,7 @@ const isWinner = (game, teamType) => {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-games-message {
|
.no-games-message {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
@@ -739,20 +877,144 @@ const isWinner = (game, teamType) => {
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
/* 胜者背景色 */
|
|
||||||
.team.winner {
|
.team.winner {
|
||||||
background-color: rgba(76, 175, 80, 0.1); /* 浅绿色背景 */
|
background-color: rgba(183, 239, 167, 0.898);
|
||||||
border-left: 3px solid #74fd79; /* 左侧绿色边框 */
|
border-left: 3px solid #a4b1ee;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 如果希望更明显的效果,可以调整样式 */
|
|
||||||
.team.winner .team-name {
|
.team.winner .team-name {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #2e7d32; /* 深绿色文字 */
|
color: #d4e66a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.team.winner .team-score {
|
.team.winner .team-score {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #2e7d32; /* 深绿色比分 */
|
color: #68db81;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
display: flex;
|
||||||
|
padding: 5px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border: 1px solid #1d428a;
|
||||||
|
background: #1d428a;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-email {
|
||||||
|
color: #1d428a;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid #e74c3c;
|
||||||
|
background: #e74c3c;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #0011ff;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.ad{
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
/* border: 1px solid #79d0e4; */
|
||||||
|
border-radius: 6px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px){
|
||||||
|
.zfb{
|
||||||
|
z-index: 9;
|
||||||
|
position: fixed;
|
||||||
|
img{
|
||||||
|
width: auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.zfb{
|
||||||
|
position: fixed;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
img{
|
||||||
|
width: auto;
|
||||||
|
height: 400px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.zfb-text {
|
||||||
|
margin: 10px 0 5px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.close-btn {
|
||||||
|
margin-top: 5px;
|
||||||
|
background: rgba(220, 53, 69, 0.9);
|
||||||
|
color: white;
|
||||||
|
border: 2px solid white;
|
||||||
|
border-radius: 6px;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
padding: 8px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PC端:右下角稍微上面一点 */
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.zfb {
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
top: auto;
|
||||||
|
left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端:居中 */
|
||||||
|
@media (max-width: 768px){
|
||||||
|
.zfb{
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
right: auto;
|
||||||
|
bottom: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
120
src/main.js
120
src/main.js
@@ -4,9 +4,127 @@ import 'element-plus/dist/index.css'
|
|||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
import { useUserStore } from '@/stores/user'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(ElementPlus)
|
app.use(ElementPlus)
|
||||||
app.use(createPinia())
|
app.use(pinia)
|
||||||
|
|
||||||
|
// 应用启动时尝试根据后端 session 恢复登录态(需要后端正确返回 Set-Cookie)
|
||||||
|
const userStore = useUserStore(pinia)
|
||||||
|
userStore.fetchCurrentUser().catch(() => {
|
||||||
|
// 静默失败,维持未登录状态
|
||||||
|
})
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|
||||||
|
// ------------------ DevTools 防护(弱保护,易被绕过) ------------------
|
||||||
|
// 说明:浏览器端无法绝对禁止 DevTools,以下脚本只是拦截常见快捷键并检测 DevTools 打开,
|
||||||
|
// 在检测到时展示一个覆盖提示,增加逆向门槛,但有经验者仍可绕过。
|
||||||
|
|
||||||
|
// 拦截常见的打开 DevTools / 查看源码 的快捷键
|
||||||
|
// function preventDevShortcuts(e) {
|
||||||
|
// // F12
|
||||||
|
// if (e.keyCode === 123) {
|
||||||
|
// e.preventDefault()
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// // Ctrl+Shift+I / Ctrl+Shift+J / Ctrl+U / Ctrl+S
|
||||||
|
// if (e.ctrlKey && e.shiftKey && (e.key === 'I' || e.key === 'i' || e.key === 'J' || e.key === 'j')) {
|
||||||
|
// e.preventDefault()
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// if (e.ctrlKey && (e.key === 'U' || e.key === 'u' || e.key === 'S' || e.key === 's')) {
|
||||||
|
// e.preventDefault()
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 禁用右键菜单(可选)
|
||||||
|
// function preventContextMenu(e) {
|
||||||
|
// // 如果你不想全局禁止右键,可以把这个函数内的逻辑改为更精确的判断
|
||||||
|
// // e.preventDefault()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 检测 DevTools 是否打开(基于窗口尺寸差异的启发式方法)
|
||||||
|
// function createDevToolsDetector(onOpen, onClose) {
|
||||||
|
|
||||||
|
// let devtoolsOpen = false
|
||||||
|
// const threshold = 160 // 阈值,视浏览器和平台可调整
|
||||||
|
|
||||||
|
// function scan() {
|
||||||
|
// try {
|
||||||
|
// const widthDiff = window.outerWidth - window.innerWidth
|
||||||
|
// const heightDiff = window.outerHeight - window.innerHeight
|
||||||
|
// const currentlyOpen = widthDiff > threshold || heightDiff > threshold
|
||||||
|
// if (currentlyOpen && !devtoolsOpen) {
|
||||||
|
// devtoolsOpen = true
|
||||||
|
// onOpen && onOpen()
|
||||||
|
// } else if (!currentlyOpen && devtoolsOpen) {
|
||||||
|
// devtoolsOpen = false
|
||||||
|
// onClose && onClose()
|
||||||
|
// }
|
||||||
|
// } catch (e) {
|
||||||
|
// // 忽略
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const id = setInterval(scan, 800)
|
||||||
|
// // 返回停止检测的函数
|
||||||
|
// return () => clearInterval(id)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 显示覆盖提示(简单实现)
|
||||||
|
// function showOverlayMessage() {
|
||||||
|
// debugger
|
||||||
|
// if (document.getElementById('__devtools_block_overlay')) return
|
||||||
|
// const o = document.createElement('div')
|
||||||
|
// o.id = '__devtools_block_overlay'
|
||||||
|
// Object.assign(o.style, {
|
||||||
|
// position: 'fixed',
|
||||||
|
// inset: '0',
|
||||||
|
// background: 'rgba(0,0,0,0.7)',
|
||||||
|
// color: '#fff',
|
||||||
|
// display: 'flex',
|
||||||
|
// alignItems: 'center',
|
||||||
|
// justifyContent: 'center',
|
||||||
|
// zIndex: '999999',
|
||||||
|
// textAlign: 'center',
|
||||||
|
// padding: '20px'
|
||||||
|
// })
|
||||||
|
// o.innerHTML = `<div style="max-width:560px"><h2 style="margin:0 0 12px;font-size:20px">检测到开发者工具</h2><p style="margin:0 0 18px;color:#ddd">为保护内容,当前页面在检测到开发者工具打开时被屏蔽。若要继续,请刷新页面或关闭开发者工具。</p><button id="__devtools_block_close" style="padding:10px 16px;border-radius:6px;border:none;background:#1890ff;color:#fff;font-weight:600;cursor:pointer">刷新页面</button></div>`
|
||||||
|
// document.body.appendChild(o)
|
||||||
|
// const btn = document.getElementById('__devtools_block_close')
|
||||||
|
// if (btn) btn.addEventListener('click', () => location.reload())
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function removeOverlayMessage() {
|
||||||
|
// const el = document.getElementById('__devtools_block_overlay')
|
||||||
|
// if (el) el.remove()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 启动防护(放在监听器里以避免 SSR 问题)
|
||||||
|
// if (typeof window !== 'undefined') {
|
||||||
|
// window.addEventListener('keydown', preventDevShortcuts, { capture: true })
|
||||||
|
// window.addEventListener('contextmenu', preventContextMenu, { capture: true })
|
||||||
|
|
||||||
|
// const stopDetector = createDevToolsDetector(() => {
|
||||||
|
// // 打开时触发:显示覆盖提示
|
||||||
|
// try { showOverlayMessage() } catch (e) {}
|
||||||
|
// }, () => {
|
||||||
|
// // 关闭时触发:移除覆盖
|
||||||
|
// try { removeOverlayMessage() } catch (e) {}
|
||||||
|
// })
|
||||||
|
|
||||||
|
// // 可选:在页面卸载时清理
|
||||||
|
// window.addEventListener('beforeunload', () => {
|
||||||
|
// window.removeEventListener('keydown', preventDevShortcuts, { capture: true })
|
||||||
|
// window.removeEventListener('contextmenu', preventContextMenu, { capture: true })
|
||||||
|
// stopDetector()
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ------------------ End DevTools 防护 ------------------
|
||||||
|
|||||||
@@ -11,23 +11,39 @@ const routes = [
|
|||||||
path: '/',
|
path: '/',
|
||||||
name: 'Index',
|
name: 'Index',
|
||||||
component: IndexVue,
|
component: IndexVue,
|
||||||
props: route => ({ query: route.query })
|
props: route => ({ query: route.query }),
|
||||||
|
meta: {
|
||||||
|
title: '首页 - NBA 在线观看与赛程',
|
||||||
|
description: '首页 - 提供最新的NBA赛程、比赛直播入口与集锦,在线观看NBA赛事。'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/play/:gameId',
|
path: '/play/:gameId',
|
||||||
name: 'Play',
|
name: 'Play',
|
||||||
component: () => import('@/views/Play.vue'),
|
component: () => import('@/views/Play.vue'),
|
||||||
props: true // 启用props接收路由参数
|
props: true, // 启用props接收路由参数
|
||||||
|
meta: {
|
||||||
|
title: '比赛直播 - NBA 在线观看',
|
||||||
|
description: '在线观看赛事直播,进入播放页观看指定比赛的实时直播与回放。'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/lives',
|
path: '/lives',
|
||||||
name: 'Admin',
|
name: 'Admin',
|
||||||
component: AdminVue,
|
component: AdminVue,
|
||||||
|
meta: {
|
||||||
|
title: '直播列表 - NBA 在线观看',
|
||||||
|
description: '直播列表 - 列出当前正在进行或即将进行的NBA比赛直播。'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/test',
|
path: '/test',
|
||||||
name: 'Test',
|
name: 'Test',
|
||||||
component: TestVue,
|
component: TestVue,
|
||||||
|
meta: {
|
||||||
|
title: '测试页面',
|
||||||
|
description: '测试页面 - 开发与测试用途。'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// 添加通配符路由,捕获所有未匹配的路径
|
// 添加通配符路由,捕获所有未匹配的路径
|
||||||
{
|
{
|
||||||
@@ -42,7 +58,7 @@ const router = createRouter({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 添加全局路由守卫
|
// 添加全局路由守卫
|
||||||
router.beforeEach((to, from) => {
|
router.beforeEach(async (to, from) => {
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
|
|
||||||
// 离开播放页时清理数据
|
// 离开播放页时清理数据
|
||||||
@@ -54,6 +70,70 @@ router.beforeEach((to, from) => {
|
|||||||
if (to.name === 'Play' && !gameStore.currentGame) {
|
if (to.name === 'Play' && !gameStore.currentGame) {
|
||||||
return '/' // 无数据则重定向到首页
|
return '/' // 无数据则重定向到首页
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 进入 Admin 页面时检查认证
|
||||||
|
if (to.name === 'Admin') {
|
||||||
|
const storedPassword = localStorage.getItem('password')
|
||||||
|
|
||||||
|
// 如果已保存正确密码,则允许进入
|
||||||
|
if (storedPassword && storedPassword === 'inspur123') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 需要输入密码
|
||||||
|
const password = prompt('请输入密码:')
|
||||||
|
|
||||||
|
// 用户取消或输入为空
|
||||||
|
if (password === null || password === '') {
|
||||||
|
return false // 阻止进入,留在当前页面
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码
|
||||||
|
try {
|
||||||
|
const { go } = await import('@/api/nba.js')
|
||||||
|
const response = await go(password)
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
localStorage.setItem('password', password)
|
||||||
|
return true // 允许进入
|
||||||
|
} else {
|
||||||
|
alert('密码错误')
|
||||||
|
return false // 密码错误,阻止进入
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('认证失败:', error)
|
||||||
|
return false // 认证失败,阻止进入
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 在导航完成后设置页面标题与 description(方便 SPA 被搜索引擎抓取时提供合适的 meta)
|
||||||
|
router.afterEach((to) => {
|
||||||
|
try {
|
||||||
|
const defaultTitle = 'NBA在线观看,NBA免费直播'
|
||||||
|
const title = (to.meta && to.meta.title) ? to.meta.title : defaultTitle
|
||||||
|
document.title = title
|
||||||
|
|
||||||
|
// 更新 meta description
|
||||||
|
const descContent = (to.meta && to.meta.description) ? to.meta.description : document.querySelector('meta[name="description"]')?.getAttribute('content') || ''
|
||||||
|
let desc = document.querySelector('meta[name="description"]')
|
||||||
|
if (!desc) {
|
||||||
|
desc = document.createElement('meta')
|
||||||
|
desc.setAttribute('name', 'description')
|
||||||
|
document.head.appendChild(desc)
|
||||||
|
}
|
||||||
|
desc.setAttribute('content', descContent)
|
||||||
|
|
||||||
|
// 更新 canonical 为当前页面(相对 origin)
|
||||||
|
const canonical = document.querySelector('link[rel="canonical"]') || document.createElement('link')
|
||||||
|
canonical.setAttribute('rel', 'canonical')
|
||||||
|
// 使用 location.origin 作为域名,当部署后会替换为真实域名
|
||||||
|
canonical.setAttribute('href', location.origin + to.fullPath)
|
||||||
|
if (!document.querySelector('link[rel="canonical"]')) document.head.appendChild(canonical)
|
||||||
|
} catch (e) {
|
||||||
|
// 不阻塞路由,仅记录错误
|
||||||
|
// console.warn('更新 meta 失败', e)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
@@ -2,37 +2,38 @@ import { defineStore } from 'pinia'
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
export const useGameStore = defineStore('game', () => {
|
export const useGameStore = defineStore('game', () => {
|
||||||
// 状态
|
|
||||||
const _currentGame = ref(null)
|
const _currentGame = ref(null)
|
||||||
const _currentStream = ref(null)
|
const _currentStream = ref(null)
|
||||||
const _allStreams = ref([])
|
const _allStreams = ref([])
|
||||||
|
const _gameId = ref(null)
|
||||||
|
|
||||||
// 计算属性(保持响应式)
|
|
||||||
const currentGame = computed(() => _currentGame.value)
|
const currentGame = computed(() => _currentGame.value)
|
||||||
const allStreams = computed(() => _allStreams.value)
|
const allStreams = computed(() => _allStreams.value)
|
||||||
|
const gameId = computed(() => _gameId.value)
|
||||||
const currentStream = computed({
|
const currentStream = computed({
|
||||||
get: () => _currentStream.value,
|
get: () => _currentStream.value,
|
||||||
set: (val) => _currentStream.value = val // 确保可写
|
set: (val) => _currentStream.value = val
|
||||||
})
|
})
|
||||||
|
|
||||||
// 设置当前比赛
|
|
||||||
const setCurrentGame = (data) => {
|
const setCurrentGame = (data) => {
|
||||||
_currentGame.value = data.gameData
|
_currentGame.value = data.gameData
|
||||||
_currentStream.value = data.currentStream
|
_currentStream.value = data.currentStream
|
||||||
_allStreams.value = data.allStreams
|
_allStreams.value = data.allStreams
|
||||||
|
_gameId.value = data.gameId
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除数据
|
|
||||||
const clearGameData = () => {
|
const clearGameData = () => {
|
||||||
_currentGame.value = null
|
_currentGame.value = null
|
||||||
_currentStream.value = null
|
_currentStream.value = null
|
||||||
_allStreams.value = []
|
_allStreams.value = []
|
||||||
|
_gameId.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentGame,
|
currentGame,
|
||||||
currentStream,
|
currentStream,
|
||||||
allStreams,
|
allStreams,
|
||||||
|
gameId,
|
||||||
setCurrentGame,
|
setCurrentGame,
|
||||||
clearGameData
|
clearGameData
|
||||||
}
|
}
|
||||||
|
|||||||
86
src/stores/user.js
Normal file
86
src/stores/user.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import {
|
||||||
|
loginUser,
|
||||||
|
registerUser,
|
||||||
|
getCurrentUser,
|
||||||
|
logoutUser,
|
||||||
|
updatePassword,
|
||||||
|
} from '@/api/userApi' // 就是你前面写的 userApi 那个文件
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
// ===== state =====
|
||||||
|
const user = ref(null) // 当前登录用户信息
|
||||||
|
const checkedLogin = ref(false) // 是否已经检查过登录状态(用于避免每次路由都打 /me)
|
||||||
|
|
||||||
|
// ===== getters =====
|
||||||
|
const isLoggedIn = computed(() => !!user.value)
|
||||||
|
|
||||||
|
// ===== actions =====
|
||||||
|
// 1)从后端 session 里取当前用户:/user/me
|
||||||
|
const fetchCurrentUser = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getCurrentUser()
|
||||||
|
// 假设未登录时后端返回字符串 "未登录",登录时返回 NbaUser 对象
|
||||||
|
if (typeof res === 'string' && res === '未登录') {
|
||||||
|
user.value = null
|
||||||
|
} else {
|
||||||
|
user.value = res
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
user.value = null
|
||||||
|
} finally {
|
||||||
|
checkedLogin.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2)登录:调 /user/login,再调 /user/me
|
||||||
|
const login = async ({ email, password }) => {
|
||||||
|
const msg = await loginUser({ email, password }).catch((e) => {
|
||||||
|
throw new Error("服务异常,请联系管理员")
|
||||||
|
})
|
||||||
|
if (msg !== '登录成功') {
|
||||||
|
throw new Error("登录失败,请检查邮箱或密码")
|
||||||
|
}
|
||||||
|
// 登录成功后后端已经通过 Set-Cookie 写入 JSESSIONID
|
||||||
|
// 这里再查一次当前用户,保存到 Pinia
|
||||||
|
await fetchCurrentUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3)注册:看你需求,注册后是不是自动登录
|
||||||
|
const register = async ({ email, password, code, username = '' }) => {
|
||||||
|
const msg = await registerUser({ email, password, code, username })
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.5)修改密码:带邮箱验证码
|
||||||
|
const changePassword = async ({ email, password, code }) => {
|
||||||
|
const msg = await updatePassword({ email, password, code }).catch(() => {
|
||||||
|
throw new Error('服务异常,请联系管理员')
|
||||||
|
})
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4)退出登录
|
||||||
|
const logout = async () => {
|
||||||
|
try {
|
||||||
|
await logoutUser()
|
||||||
|
} catch (e) {
|
||||||
|
// 即使后端报错,前端也要清掉状态
|
||||||
|
} finally {
|
||||||
|
user.value = null
|
||||||
|
checkedLogin.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
checkedLogin,
|
||||||
|
isLoggedIn,
|
||||||
|
fetchCurrentUser,
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
changePassword,
|
||||||
|
logout,
|
||||||
|
}
|
||||||
|
})
|
||||||
625
src/views/Admin - 副本.vue
Normal file
625
src/views/Admin - 副本.vue
Normal file
@@ -0,0 +1,625 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 直播链接区域 -->
|
||||||
|
<div v-if="getGameUrls(game.gameId).length > 0" class="urls-container">
|
||||||
|
<div v-for="(url, index) in getGameUrls(game.gameId)" :key="index" class="url-item">
|
||||||
|
<span class="url-type">{{ formatUrlType(url.type) }}:</span>
|
||||||
|
<span class="truncated-url" @click="showFullUrl(url.url)">
|
||||||
|
{{ truncateUrl(url.url) }}
|
||||||
|
</span>
|
||||||
|
<button class="check-btn" @click.stop="showFullUrl(url.url)">
|
||||||
|
<i class="el-icon-view"></i> 查看
|
||||||
|
</button>
|
||||||
|
<button class="delete-btn" @click.stop="deleteUrl(url.id)">
|
||||||
|
<i class="el-icon-delete"></i> 删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-urls">暂无直播链接</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="add-url-btn" @click="openAddUrlDialog(game)">
|
||||||
|
添加直播
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- URL详情弹窗 -->
|
||||||
|
<el-dialog v-model="urlDialogVisible" title="直播链接详情" width="50%" center>
|
||||||
|
<div class="url-dialog-content">
|
||||||
|
<div class="full-url">{{ currentUrl }}</div>
|
||||||
|
<el-button type="primary" @click="copyUrl(currentUrl)">复制链接</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 添加直播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-input-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 } from "vue";
|
||||||
|
import { ElMessage, ElLoading } from "element-plus";
|
||||||
|
import { games, urls, addUrls as apiAddUrls, go, deleteUrlById } 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);
|
||||||
|
const urlDialogVisible = ref(false);
|
||||||
|
const currentUrl = ref('');
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
const initData = async () => {
|
||||||
|
try {
|
||||||
|
const [urlsRes, gamesRes] = await Promise.all([urls(), games()]);
|
||||||
|
urlData.value = urlsRes || [];
|
||||||
|
gamesData.value = gamesRes || [];
|
||||||
|
} catch (err) {
|
||||||
|
console.error("获取数据失败:", err);
|
||||||
|
ElMessage.error("获取比赛数据失败,请刷新重试");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// URL处理函数
|
||||||
|
const truncateUrl = (url) => {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
return `${urlObj.hostname}${urlObj.pathname.substring(0, 20)}...`;
|
||||||
|
} catch {
|
||||||
|
return url.length > 30 ? `${url.substring(0, 30)}...` : url;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showFullUrl = (url) => {
|
||||||
|
currentUrl.value = url;
|
||||||
|
urlDialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyUrl = (url) => {
|
||||||
|
navigator.clipboard.writeText(url)
|
||||||
|
.then(() => {
|
||||||
|
ElMessage.success('链接已复制');
|
||||||
|
urlDialogVisible.value = false;
|
||||||
|
})
|
||||||
|
.catch(() => ElMessage.error('复制失败'));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除URL
|
||||||
|
const deleteUrl = async (id) => {
|
||||||
|
let loading = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const confirm = window.confirm('确定要删除这个直播链接吗?');
|
||||||
|
if (!confirm) return;
|
||||||
|
|
||||||
|
loading = ElLoading.service({
|
||||||
|
lock: true,
|
||||||
|
text: "正在删除直播链接...",
|
||||||
|
background: "rgba(0, 0, 0, 0.7)",
|
||||||
|
});
|
||||||
|
|
||||||
|
await deleteUrlById(id);
|
||||||
|
|
||||||
|
urlData.value = urlData.value.map(gameUrl => {
|
||||||
|
const gameId = Object.keys(gameUrl)[0];
|
||||||
|
return {
|
||||||
|
[gameId]: gameUrl[gameId].filter(url => url.id !== id)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
ElMessage.success('直播链接删除成功');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除直播链接失败:', error);
|
||||||
|
ElMessage.error(`删除失败: ${error.message || "服务器错误"}`);
|
||||||
|
} finally {
|
||||||
|
loading?.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加URL相关函数
|
||||||
|
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 isValidUrl = (url) => {
|
||||||
|
try {
|
||||||
|
new URL(url);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitUrls = async () => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
const response = await apiAddUrls(selectedGame.value.gameId, validUrls);
|
||||||
|
updateLocalUrls(selectedGame.value.gameId, response.data || validUrls);
|
||||||
|
ElMessage.success("直播链接添加成功!");
|
||||||
|
closeDialog();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("添加直播URL失败:", error);
|
||||||
|
ElMessage.error(`添加失败: ${error.message || "服务器错误"}`);
|
||||||
|
} finally {
|
||||||
|
loading.close();
|
||||||
|
isSubmitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateLocalUrls = (gameId, urlsToAdd) => {
|
||||||
|
const newUrlData = [...urlData.value];
|
||||||
|
const existingIndex = newUrlData.findIndex(item => Object.keys(item)[0] === gameId.toString());
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
const existingItem = { ...newUrlData[existingIndex] };
|
||||||
|
existingItem[gameId] = [...existingItem[gameId], ...urlsToAdd];
|
||||||
|
newUrlData[existingIndex] = existingItem;
|
||||||
|
} else {
|
||||||
|
newUrlData.push({ [gameId]: urlsToAdd });
|
||||||
|
}
|
||||||
|
|
||||||
|
urlData.value = newUrlData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
const getGameUrls = (gameId) => {
|
||||||
|
const gameUrls = urlData.value.find(item => item[gameId]);
|
||||||
|
return gameUrls ? gameUrls[gameId] : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatUrlType = (type) => {
|
||||||
|
const typeMap = {
|
||||||
|
tx: "腾讯", wl: "纬来", mg: "咪咕",
|
||||||
|
nba: "原声", zb: "其他", qq: "腾讯", wx: "微信"
|
||||||
|
};
|
||||||
|
return typeMap[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
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")}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生命周期钩子
|
||||||
|
onMounted(() => {
|
||||||
|
initData();
|
||||||
|
|
||||||
|
const storedPassword = localStorage.getItem("password");
|
||||||
|
if (storedPassword && storedPassword === "inspur123") return;
|
||||||
|
|
||||||
|
const password = prompt("请输入密码:");
|
||||||
|
go(password)
|
||||||
|
.then(response => {
|
||||||
|
if (response) {
|
||||||
|
localStorage.setItem("password", password);
|
||||||
|
} else {
|
||||||
|
window.location.href = "/";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => window.location.href = "/");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.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;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
.game-date, .game-id, .start-time {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.teams {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin: 10px 0;
|
||||||
|
|
||||||
|
.vs {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #e63946;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.urls-container {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.url-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-type {
|
||||||
|
font-weight: bold;
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.truncated-url {
|
||||||
|
flex: 1;
|
||||||
|
color: #409eff;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
margin: 0 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #66b1ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-btn {
|
||||||
|
padding: 4px 8px;
|
||||||
|
background-color: #987eff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #7d5fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background-color: #ff4444;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #cc0000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-urls {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-url-btn {
|
||||||
|
padding: 8px 15px;
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
align-self: flex-start;
|
||||||
|
|
||||||
|
&: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);
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-inputs {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-input-item {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, 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-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
padding: 8px 15px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn {
|
||||||
|
padding: 8px 15px;
|
||||||
|
background-color: #2196f3;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #0b7dda;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background-color: #cccccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* URL详情弹窗样式 */
|
||||||
|
.url-dialog-content {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.full-url {
|
||||||
|
padding: 10px;
|
||||||
|
margin: 15px 0;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -26,12 +26,15 @@
|
|||||||
<div v-if="getGameUrls(game.gameId).length > 0" class="urls-container">
|
<div v-if="getGameUrls(game.gameId).length > 0" class="urls-container">
|
||||||
<div v-for="(url, index) in getGameUrls(game.gameId)" :key="index" class="url-item">
|
<div v-for="(url, index) in getGameUrls(game.gameId)" :key="index" class="url-item">
|
||||||
<span class="url-type">{{ formatUrlType(url.type) }}:</span>
|
<span class="url-type">{{ formatUrlType(url.type) }}:</span>
|
||||||
<span class="truncated-url" @click="showFullUrl(url.url)">
|
<span class="truncated-url" @click="showFullUrl(getStreamUrl(url))">
|
||||||
{{ truncateUrl(url.url) }}
|
{{ truncateUrl(getStreamUrl(url)) }}
|
||||||
</span>
|
</span>
|
||||||
<button class="check-btn" @click.stop="showFullUrl(url.url)">
|
<button class="check-btn" @click.stop="showFullUrl(getStreamUrl(url))">
|
||||||
<i class="el-icon-view"></i> 查看
|
<i class="el-icon-view"></i> 查看
|
||||||
</button>
|
</button>
|
||||||
|
<button class="edit-btn" @click.stop="openEditUrlDialog(game.gameId, url)">
|
||||||
|
<i class="el-icon-edit"></i> 修改
|
||||||
|
</button>
|
||||||
<button class="delete-btn" @click.stop="deleteUrl(url.id)">
|
<button class="delete-btn" @click.stop="deleteUrl(url.id)">
|
||||||
<i class="el-icon-delete"></i> 删除
|
<i class="el-icon-delete"></i> 删除
|
||||||
</button>
|
</button>
|
||||||
@@ -54,6 +57,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="editDialogVisible" title="修改直播链接" width="45%" center>
|
||||||
|
<div class="edit-dialog-content">
|
||||||
|
<label>直播链接</label>
|
||||||
|
<textarea
|
||||||
|
v-model="editUrlValue"
|
||||||
|
class="edit-input edit-textarea"
|
||||||
|
placeholder="请输入完整的 m3u8 地址"
|
||||||
|
rows="4"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<button class="cancel-btn" type="button" @click="closeEditDialog">取消</button>
|
||||||
|
<button class="confirm-btn" type="button" @click="submitUrlUpdate" :disabled="editSubmitting">
|
||||||
|
<span v-if="!editSubmitting">保存</span>
|
||||||
|
<span v-else>保存中...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 添加直播URL对话框 -->
|
<!-- 添加直播URL对话框 -->
|
||||||
<div v-if="showDialog" class="dialog-overlay">
|
<div v-if="showDialog" class="dialog-overlay">
|
||||||
<div class="dialog-content">
|
<div class="dialog-content">
|
||||||
@@ -75,9 +98,9 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>直播地址 {{ index + 1 }}:</label>
|
<label>直播地址 {{ index + 1 }}:</label>
|
||||||
<input
|
<input
|
||||||
v-model="url.url"
|
v-model="url.m3u8_url"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="请输入完整的直播URL"
|
placeholder="请输入完整的 m3u8 地址"
|
||||||
class="url-input"
|
class="url-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,22 +130,26 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import { ElMessage, ElLoading } from "element-plus";
|
import { ElMessage, ElLoading } from "element-plus";
|
||||||
import { games, urls, addUrls as apiAddUrls, go, deleteUrlById } from "@/api/nba";
|
import { games, urls, addUrls as apiAddUrls, deleteUrlById, updateUrlById } from "@/api/nba";
|
||||||
|
|
||||||
// 响应式状态
|
// 响应式状态
|
||||||
const gamesData = ref([]);
|
const gamesData = ref([]);
|
||||||
const urlData = ref([]);
|
const urlData = ref([]);
|
||||||
const showDialog = ref(false);
|
const showDialog = ref(false);
|
||||||
const selectedGame = ref(null);
|
const selectedGame = ref(null);
|
||||||
const newUrls = ref([{ type: "tx", url: "" }]);
|
const newUrls = ref([{ type: "tx", m3u8_url: "" }]);
|
||||||
const isSubmitting = ref(false);
|
const isSubmitting = ref(false);
|
||||||
const urlDialogVisible = ref(false);
|
const urlDialogVisible = ref(false);
|
||||||
const currentUrl = ref('');
|
const currentUrl = ref('');
|
||||||
|
const editDialogVisible = ref(false);
|
||||||
|
const editUrlItem = ref(null);
|
||||||
|
const editUrlValue = ref('');
|
||||||
|
const editSubmitting = ref(false);
|
||||||
|
|
||||||
// 初始化数据
|
// 初始化数据
|
||||||
const initData = async () => {
|
const initData = async () => {
|
||||||
try {
|
try {
|
||||||
const [urlsRes, gamesRes] = await Promise.all([urls(), games()]);
|
const [urlsRes, gamesRes] = await Promise.all([urls("1"), games()]);
|
||||||
urlData.value = urlsRes || [];
|
urlData.value = urlsRes || [];
|
||||||
gamesData.value = gamesRes || [];
|
gamesData.value = gamesRes || [];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -132,6 +159,10 @@ const initData = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// URL处理函数
|
// URL处理函数
|
||||||
|
const getStreamUrl = (item) => {
|
||||||
|
return item?.m3u8_url || item?.url || "";
|
||||||
|
};
|
||||||
|
|
||||||
const truncateUrl = (url) => {
|
const truncateUrl = (url) => {
|
||||||
try {
|
try {
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
@@ -155,6 +186,76 @@ const copyUrl = (url) => {
|
|||||||
.catch(() => ElMessage.error('复制失败'));
|
.catch(() => ElMessage.error('复制失败'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openEditUrlDialog = (gameId, urlItem) => {
|
||||||
|
editUrlItem.value = { ...urlItem, gameId };
|
||||||
|
editUrlValue.value = urlItem?.m3u8_url || urlItem?.url || "";
|
||||||
|
editDialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeEditDialog = () => {
|
||||||
|
editDialogVisible.value = false;
|
||||||
|
editUrlItem.value = null;
|
||||||
|
editUrlValue.value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateLocalUrlItem = (gameId, id, newUrl) => {
|
||||||
|
urlData.value = urlData.value.map((gameUrl) => {
|
||||||
|
const key = Object.keys(gameUrl)[0];
|
||||||
|
if (key !== gameId.toString()) return gameUrl;
|
||||||
|
|
||||||
|
const updatedUrls = gameUrl[key].map((item) => {
|
||||||
|
if (item.id !== id) return item;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
m3u8_url: newUrl,
|
||||||
|
url: newUrl,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { [key]: updatedUrls };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitUrlUpdate = async () => {
|
||||||
|
if (!editUrlItem.value) return;
|
||||||
|
|
||||||
|
const trimmedUrl = editUrlValue.value.trim();
|
||||||
|
if (!trimmedUrl) {
|
||||||
|
ElMessage.warning('请输入有效链接');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidUrl(trimmedUrl)) {
|
||||||
|
ElMessage.warning('链接格式不正确');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editSubmitting.value = true;
|
||||||
|
const loading = ElLoading.service({
|
||||||
|
lock: true,
|
||||||
|
text: '正在修改直播链接...',
|
||||||
|
background: 'rgba(0, 0, 0, 0.7)',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateUrlById({
|
||||||
|
id: editUrlItem.value.id,
|
||||||
|
url: trimmedUrl,
|
||||||
|
gameId: editUrlItem.value.gameId,
|
||||||
|
type: editUrlItem.value.type,
|
||||||
|
});
|
||||||
|
updateLocalUrlItem(editUrlItem.value.gameId, editUrlItem.value.id, trimmedUrl);
|
||||||
|
ElMessage.success('直播链接修改成功');
|
||||||
|
closeEditDialog();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('修改直播URL失败:', error);
|
||||||
|
ElMessage.error(`修改失败: ${error.message || '服务器错误'}`);
|
||||||
|
} finally {
|
||||||
|
loading.close();
|
||||||
|
editSubmitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 删除URL
|
// 删除URL
|
||||||
const deleteUrl = async (id) => {
|
const deleteUrl = async (id) => {
|
||||||
let loading = null;
|
let loading = null;
|
||||||
@@ -190,7 +291,7 @@ const deleteUrl = async (id) => {
|
|||||||
// 添加URL相关函数
|
// 添加URL相关函数
|
||||||
const openAddUrlDialog = (game) => {
|
const openAddUrlDialog = (game) => {
|
||||||
selectedGame.value = game;
|
selectedGame.value = game;
|
||||||
newUrls.value = [{ type: "tx", url: "" }];
|
newUrls.value = [{ type: "tx", m3u8_url: "" }];
|
||||||
showDialog.value = true;
|
showDialog.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -199,7 +300,7 @@ const closeDialog = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const addMoreUrl = () => {
|
const addMoreUrl = () => {
|
||||||
newUrls.value.push({ type: "tx", url: "" });
|
newUrls.value.push({ type: "tx", m3u8_url: "" });
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeUrl = (index) => {
|
const removeUrl = (index) => {
|
||||||
@@ -217,10 +318,10 @@ const isValidUrl = (url) => {
|
|||||||
|
|
||||||
const submitUrls = async () => {
|
const submitUrls = async () => {
|
||||||
const validUrls = newUrls.value
|
const validUrls = newUrls.value
|
||||||
.filter(item => item.url.trim() !== "")
|
.filter(item => item.m3u8_url.trim() !== "")
|
||||||
.map(item => ({
|
.map(item => ({
|
||||||
type: item.type,
|
type: item.type,
|
||||||
url: item.url.trim(),
|
m3u8_url: item.m3u8_url.trim(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (validUrls.length === 0) {
|
if (validUrls.length === 0) {
|
||||||
@@ -229,8 +330,8 @@ const submitUrls = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const url of validUrls) {
|
for (const url of validUrls) {
|
||||||
if (!isValidUrl(url.url)) {
|
if (!isValidUrl(url.m3u8_url)) {
|
||||||
ElMessage.warning(`直播地址格式不正确: ${url.url}`);
|
ElMessage.warning(`直播地址格式不正确: ${url.m3u8_url}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,21 +398,8 @@ const formatTime = (timeString) => {
|
|||||||
|
|
||||||
// 生命周期钩子
|
// 生命周期钩子
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 认证已在路由守卫中进行,此处只需加载数据
|
||||||
initData();
|
initData();
|
||||||
|
|
||||||
const storedPassword = localStorage.getItem("password");
|
|
||||||
if (storedPassword && storedPassword === "inspur123") return;
|
|
||||||
|
|
||||||
const password = prompt("请输入密码:");
|
|
||||||
go(password)
|
|
||||||
.then(response => {
|
|
||||||
if (response) {
|
|
||||||
localStorage.setItem("password", password);
|
|
||||||
} else {
|
|
||||||
window.location.href = "/";
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => window.location.href = "/");
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -439,6 +527,21 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-btn {
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background-color: #ffc107;
|
||||||
|
color: #333;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #e0a800;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.delete-btn {
|
.delete-btn {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
@@ -456,6 +559,49 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-dialog-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-dialog-content label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-input {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 60px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #dcdcdc;
|
||||||
|
border-radius: 6px;
|
||||||
|
outline: none;
|
||||||
|
font-size: 14px;
|
||||||
|
resize: vertical;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-textarea {
|
||||||
|
min-height: 120px;
|
||||||
|
max-height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer .cancel-btn,
|
||||||
|
.dialog-footer .confirm-btn {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.no-urls {
|
.no-urls {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
color: #999;
|
color: #999;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<NBASchedule
|
<NBASchedule
|
||||||
:scheduleData="scheduleData"
|
:scheduleData="scheduleData"
|
||||||
@@ -8,17 +8,53 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<el-backtop :right="50" :bottom="50" />
|
<el-backtop :right="50" :bottom="50" />
|
||||||
|
|
||||||
|
<!-- 公告弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="showAnnouncement"
|
||||||
|
title=""
|
||||||
|
width="90%"
|
||||||
|
:close-on-click-modal="true"
|
||||||
|
:show-close="false"
|
||||||
|
class="announcement-dialog"
|
||||||
|
>
|
||||||
|
<div class="announcement-content">
|
||||||
|
<!-- <p>欢迎访问 NBA 直播!</p> -->
|
||||||
|
<!-- <p>欢迎访问共享赛事直播平台</p> -->
|
||||||
|
<!-- <p>主域名:jrs77.xyz </p> -->
|
||||||
|
<p>观看人数过多会出现卡顿情况</p>
|
||||||
|
<p>出现卡顿,推荐开启VPN选择美国、香港节点观看</p>
|
||||||
|
<p>暂只支持登录用户观看</p>
|
||||||
|
<!-- <p>网站维护中,暂时无法观赛,预计1-2天恢复</p> -->
|
||||||
|
<p>发布页:2026123.xyz(建议收藏)</p>
|
||||||
|
<!-- <p>祝您观赛愉快!</p> -->
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<div class="announcement-footer">
|
||||||
|
<el-button type="primary" class="announcement-confirm" @click="showAnnouncement = false">确认</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import NBASchedule from "@/components/NBASchedule.vue";
|
import NBASchedule from "@/components/NBASchedule.vue";
|
||||||
import { schedule,games } from "@/api/nba";
|
import { schedule,games } from "@/api/nba";
|
||||||
|
|
||||||
const scheduleData = ref(null);
|
const scheduleData = ref(null);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const error = ref(null);
|
const error = ref(null);
|
||||||
|
const showAnnouncement = ref(false);
|
||||||
|
|
||||||
|
// 检查并显示公告
|
||||||
|
onMounted(() => {
|
||||||
|
if (!sessionStorage.getItem('announcement_shown')) {
|
||||||
|
showAnnouncement.value = true;
|
||||||
|
sessionStorage.setItem('announcement_shown', 'true');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 获取当前时间戳(秒)
|
// 获取当前时间戳(秒)
|
||||||
const currentTimestamp = Math.floor(Date.now() / 1000);
|
const currentTimestamp = Math.floor(Date.now() / 1000);
|
||||||
@@ -71,5 +107,107 @@
|
|||||||
fetchScheduleData();
|
fetchScheduleData();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
/* 将 dialog 居中显示 */
|
||||||
|
:deep(.el-dialog) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 500px;
|
||||||
|
position: absolute !important;
|
||||||
|
top: 50% !important;
|
||||||
|
left: 50% !important;
|
||||||
|
transform: translate(-50%, -50%) !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 小屏幕时宽度微调 */
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
:deep(.el-dialog) {
|
||||||
|
width: 90% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-content {
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.announcement-content p {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部确认按钮容器,居中显示 */
|
||||||
|
.announcement-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 矩形确认按钮样式 */
|
||||||
|
.announcement-confirm {
|
||||||
|
min-width: 120px;
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 40px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题居中,同时保留关闭按钮在右 */
|
||||||
|
:deep(.el-dialog__header) {
|
||||||
|
position: relative;
|
||||||
|
padding: 12px 68px 12px 16px; /* 增加右侧内边距,给更大的关闭按钮留位 */
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__title) {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 突出关闭按钮样式:更大尺寸、蓝色主题、悬停放大 */
|
||||||
|
:deep(.el-dialog__headerbtn) {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 8px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #1890ff; /* 调整为蓝色 */
|
||||||
|
box-shadow: 0 4px 12px rgba(24,144,255,0.18);
|
||||||
|
transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__headerbtn):hover {
|
||||||
|
transform: scale(1.06);
|
||||||
|
box-shadow: 0 6px 18px rgba(24,144,255,0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-dialog__headerbtn) :deep(.el-icon) {
|
||||||
|
color: #fff !important;
|
||||||
|
font-size: 18px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 小屏幕下微调关闭按钮 */
|
||||||
|
@media screen and (max-width: 420px) {
|
||||||
|
:deep(.el-dialog__headerbtn) {
|
||||||
|
right: 6px;
|
||||||
|
top: 6px;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
:deep(.el-dialog__title) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
@@ -15,4 +15,22 @@ export default defineConfig({
|
|||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
// target: 'http://116.62.173.2:9005',
|
||||||
|
target: 'http://localhost:9005',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
||||||
|
},
|
||||||
|
'/user': {
|
||||||
|
// target: 'http://116.62.173.2:9005',
|
||||||
|
target: 'http://localhost:9005',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
rewrite: (path) => path.replace(/^\/user/, '/user'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user