diff --git a/package.json b/package.json index 8d8a0a3..4103ffb 100644 --- a/package.json +++ b/package.json @@ -42,11 +42,13 @@ "react-dom": "^18.2.0", "react-icons": "^5.4.0", "redis": "^4.6.7", + "sweetalert2": "^11.11.0", "swiper": "^11.2.8", "tailwind-merge": "^2.6.0", "vidstack": "^0.6.15", - "zod": "^3.24.1", - "sweetalert2": "^11.11.0" + "xgplayer": "^3.0.22", + "xgplayer-hls.js": "^3.0.22", + "zod": "^3.24.1" }, "devDependencies": { "@commitlint/cli": "^16.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d405333..46a6bd1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,12 @@ importers: vidstack: specifier: ^0.6.15 version: 0.6.15 + xgplayer: + specifier: ^3.0.22 + version: 3.0.22(core-js@3.43.0) + xgplayer-hls.js: + specifier: ^3.0.22 + version: 3.0.22(core-js@3.43.0)(xgplayer@3.0.22(core-js@3.43.0)) zod: specifier: ^3.24.1 version: 3.25.67 @@ -2811,6 +2817,9 @@ packages: core-js-compat@3.43.0: resolution: {integrity: sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==} + core-js@3.43.0: + resolution: {integrity: sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==} + cosmiconfig-typescript-loader@2.0.2: resolution: {integrity: sha512-KmE+bMjWMXJbkWCeY4FJX/npHuZPNr9XF9q9CIQ/bpFwi1qHfCmSiKarrCcRa0LO4fWjk93pVoeRtJAkTGcYNw==} engines: {node: '>=12', npm: '>=6'} @@ -2883,9 +2892,16 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} + damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + danmu.js@1.1.13: + resolution: {integrity: sha512-knFd0/cB2HA4FFWiA7eB2suc5vCvoHdqio33FyyCSfP7C+1A+zQcTvnvwfxaZhrxsGj4qaQI2I8XiTqedRaVmg==} + dargs@7.0.0: resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==} engines: {node: '>=8'} @@ -2952,6 +2968,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@2.0.1: + resolution: {integrity: sha512-VIPwiMJqJ13ZQfaCsIFnp5Me9tnjURiaIFxfz7EH0Ci0dTSQpZtSLrqOicXqEd/z2r+z+Klk9GzmnRsgpgbOsQ==} + engines: {node: '>=0.10.0'} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -2975,6 +2995,9 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + delegate@3.2.0: + resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==} + depd@1.1.2: resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} engines: {node: '>= 0.6'} @@ -3045,6 +3068,9 @@ packages: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} + downloadjs@1.4.7: + resolution: {integrity: sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -3131,6 +3157,17 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + + es6-iterator@2.0.3: + resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + + es6-symbol@3.1.4: + resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} + engines: {node: '>=0.12'} + esbuild-android-64@0.14.47: resolution: {integrity: sha512-R13Bd9+tqLVFndncMHssZrPWe6/0Kpv2/dt4aA69soX4PRxlzsVpCvoJeFE8sOEoeVEiBkI0myjlkDodXlHa0g==} engines: {node: '>=12'} @@ -3520,6 +3557,10 @@ packages: deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true + esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + espree@9.6.1: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3559,6 +3600,12 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + event-emitter@0.3.5: + resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + events-intercept@2.0.0: resolution: {integrity: sha512-blk1va0zol9QOrdZt0rFXo5KMkNPVSp92Eju/Qz8THwKWKRKeE0T8Br/1aW6+Edkyq9xHYgYxn2QtOnUKPUp+Q==} @@ -3589,6 +3636,9 @@ packages: exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + ext@1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -4747,6 +4797,9 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + next-tick@1.1.0: + resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + next@14.2.30: resolution: {integrity: sha512-+COdu6HQrHHFQ1S/8BBsCag61jZacmvbuL2avHvQFbWa2Ox7bE+d8FyNgxRLjXQ5wtPyQwEmk85js/AuaG2Sbg==} engines: {node: '>=18.17.0'} @@ -5957,6 +6010,9 @@ packages: resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} engines: {node: '>=14.16'} + type@2.7.3: + resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -6303,6 +6359,22 @@ packages: resolution: {integrity: sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw==} engines: {node: '>= 6.0'} + xgplayer-hls.js@3.0.22: + resolution: {integrity: sha512-oNhiw1QnWNYmZsKnq7D8PUfAVHqAwv4kCwEwARMquGcUEZwNOOvazzZopTYPgwXj965LKSzgD36nhpbfYef/ZQ==} + peerDependencies: + core-js: '>=3.12.1' + xgplayer: '>=3.0.13' + + xgplayer-subtitles@3.0.22: + resolution: {integrity: sha512-2XjamtZnWS/r4QjesOC34JmuGD3QPbgeqkI4t5Gq19dN1CWNBP7nJ8pbGLuAeHswKjGg8LFRpnsic7xjc/XSyA==} + peerDependencies: + core-js: '>=3.12.1' + + xgplayer@3.0.22: + resolution: {integrity: sha512-uVKffa02NxWnWMVzgnrU0HGwZFH0ymPHsD3zGxtV6oPPplA6EBLyh9N5q3b++J7jRs2usvKR2+WslT+je1RuwA==} + peerDependencies: + core-js: '>=3.12.1' + xml-name-validator@3.0.0: resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==} @@ -9355,6 +9427,8 @@ snapshots: dependencies: browserslist: 4.25.0 + core-js@3.43.0: {} + cosmiconfig-typescript-loader@2.0.2(@types/node@24.0.3)(cosmiconfig@7.1.0)(typescript@4.9.5): dependencies: '@types/node': 24.0.3 @@ -9430,8 +9504,17 @@ snapshots: csstype@3.1.3: {} + d@1.0.2: + dependencies: + es5-ext: 0.10.64 + type: 2.7.3 + damerau-levenshtein@1.0.8: {} + danmu.js@1.1.13: + dependencies: + event-emitter: 0.3.5 + dargs@7.0.0: {} data-uri-to-buffer@2.0.2: {} @@ -9487,6 +9570,8 @@ snapshots: deep-is@0.1.4: {} + deepmerge@2.0.1: {} + deepmerge@4.3.1: {} define-data-property@1.1.4: @@ -9515,6 +9600,8 @@ snapshots: delayed-stream@1.0.0: {} + delegate@3.2.0: {} + depd@1.1.2: {} dequal@2.0.3: {} @@ -9576,6 +9663,8 @@ snapshots: dependencies: is-obj: 2.0.0 + downloadjs@1.4.7: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -9730,6 +9819,24 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es5-ext@0.10.64: + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + esniff: 2.0.1 + next-tick: 1.1.0 + + es6-iterator@2.0.3: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-symbol: 3.1.4 + + es6-symbol@3.1.4: + dependencies: + d: 1.0.2 + ext: 1.7.0 + esbuild-android-64@0.14.47: optional: true @@ -10140,6 +10247,13 @@ snapshots: transitivePeerDependencies: - supports-color + esniff@2.0.1: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + type: 2.7.3 + espree@9.6.1: dependencies: acorn: 8.15.0 @@ -10168,6 +10282,13 @@ snapshots: etag@1.8.1: {} + event-emitter@0.3.5: + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + + eventemitter3@4.0.7: {} + events-intercept@2.0.0: {} events@3.3.0: {} @@ -10206,6 +10327,10 @@ snapshots: exsolve@1.0.7: {} + ext@1.7.0: + dependencies: + type: 2.7.3 + fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -11646,6 +11771,8 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + next-tick@1.1.0: {} + next@14.2.30(@babel/core@7.27.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 14.2.30 @@ -12850,6 +12977,8 @@ snapshots: type-fest@3.13.1: {} + type@2.7.3: {} + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -13361,6 +13490,28 @@ snapshots: dependencies: os-paths: 4.4.0 + xgplayer-hls.js@3.0.22(core-js@3.43.0)(xgplayer@3.0.22(core-js@3.43.0)): + dependencies: + core-js: 3.43.0 + deepmerge: 2.0.1 + event-emitter: 0.3.5 + hls.js: 1.6.5 + xgplayer: 3.0.22(core-js@3.43.0) + + xgplayer-subtitles@3.0.22(core-js@3.43.0): + dependencies: + core-js: 3.43.0 + eventemitter3: 4.0.7 + + xgplayer@3.0.22(core-js@3.43.0): + dependencies: + core-js: 3.43.0 + danmu.js: 1.1.13 + delegate: 3.2.0 + downloadjs: 1.4.7 + eventemitter3: 4.0.7 + xgplayer-subtitles: 3.0.22(core-js@3.43.0) + xml-name-validator@3.0.0: {} xmlchars@2.2.0: {} diff --git a/src/app/new-play/page.tsx b/src/app/new-play/page.tsx new file mode 100644 index 0000000..458b4e7 --- /dev/null +++ b/src/app/new-play/page.tsx @@ -0,0 +1,250 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment, @typescript-eslint/no-explicit-any, react-hooks/exhaustive-deps, no-console, @next/next/no-img-element */ + +'use client'; + +import { Suspense, useEffect, useRef } from 'react'; + +import 'xgplayer/dist/index.min.css'; + +import EpisodeSelector from '@/components/EpisodeSelector'; +import PageLayout from '@/components/PageLayout'; + +function PlayPageClient() { + const playerRef = useRef(null); + + useEffect(() => { + const initPlayer = async () => { + try { + // 动态导入 xgplayer 和 HLS 插件 + const [{ default: Player }, { default: HLS }] = await Promise.all([ + import('xgplayer'), + import('xgplayer-hls.js'), + ]); + + console.log('Player:', Player); + console.log('HLS Plugin:', HLS); + + // 初始化 xgplayer + const player = new Player({ + id: 'player-container', + // 示例 HLS 流地址,您可以根据需要替换 + url: 'https://vip.dytt-cinema.com/20250607/24158_8e0c4a20/index.m3u8', + width: '100%', + height: '100%', + // 播放器配置 + autoplay: true, + preload: 'auto', + // 禁用响应式设计,保持原始大小 + fluid: false, + // 保持视频原始尺寸和宽高比 + videoFillMode: 'contain', + // 禁用自动调整尺寸 + autoResize: false, + // 保持原始视频比例 + aspectRatio: 'auto', + // 控制栏配置 + controls: true, + // 海报图片 + poster: '', + // 其他配置 + playsinline: true, + lang: 'zh-cn', + volume: 0.8, + // 插件配置 - 注册 HLS 插件 + plugins: [HLS], + // HLS 特定配置 + hls: { + // 使用 hls.js 引擎 + enableWorker: true, + // 预加载配置 + maxBufferLength: 30, + maxBufferSize: 60 * 1000 * 1000, + // 错误处理 + enableSoftwareAES: true, + debug: process.env.NODE_ENV === 'development', + // 自适应码率 + enableLowInitialPlaylist: false, + // 跨域配置 + withCredentials: false, + // 错误恢复 + maxMaxBufferLength: 600, + backBufferLength: 90, + // 性能优化 + liveSyncDurationCount: 3, + liveMaxLatencyDurationCount: 10, + }, + }); + + // 保存播放器实例 + playerRef.current = player; + + // 添加事件监听器 + player.on('ready', () => { + console.log('播放器已准备就绪'); + }); + + player.on('play', () => { + console.log('开始播放'); + }); + + player.on('pause', () => { + console.log('暂停播放'); + }); + + player.on('error', (err: any) => { + console.error('播放器错误:', err); + }); + + // HLS 特定事件 + player.on('hls_manifest_parsed', () => { + console.log('HLS 清单解析完成'); + }); + + player.on('hls_level_switched', (event: any) => { + console.log('HLS 质量切换:', event); + }); + + player.on('hls_media_attached', () => { + console.log('HLS 媒体已附加'); + }); + + player.on('hls_manifest_loaded', () => { + console.log('HLS 清单已加载'); + }); + + player.on('hls_level_loaded', () => { + console.log('HLS 级别已加载'); + }); + + player.on('hls_frag_loaded', () => { + console.log('HLS 片段已加载'); + }); + + player.on('hls_error', (err: any) => { + console.error('HLS 错误:', err); + }); + } catch (error) { + console.error('初始化播放器失败:', error); + } + }; + + // 初始化播放器 + initPlayer(); + + // 清理函数 + return () => { + if (playerRef.current) { + try { + playerRef.current.destroy(); + } catch (error) { + console.error('销毁播放器失败:', error); + } + } + }; + }, []); + + return ( + +
+ {/* 第一行:影片标题 */} +
+

+ 影片标题 +

+
+ + {/* 第二行:播放器和选集 */} +
+ {/* 播放器 */} +
+
+
+ + {/* 选集 */} +
+ +
+
+ + {/* 海报(移动端在上方) */} +
+
+
+ 封面图片 +
+
+
+ + {/* 详情展示 */} +
+ {/* 文字区 */} +
+
+

影片详情

+
+
+

剧情简介

+

+ 这里是影片的详细剧情介绍。可以包含影片的背景、故事梗概、主要人物关系等信息。 + 文字内容可以根据实际的影片数据动态显示。 +

+
+
+

基本信息

+
+
+ 导演: + + 待获取 + +
+
+ 主演: + + 待获取 + +
+
+ 类型: + + 待获取 + +
+
+ 年份: + + 待获取 + +
+
+
+
+
+
+ + {/* 封面展示(桌面端在右侧) */} +
+
+
+ + 封面图片 + +
+
+
+
+
+
+ ); +} + +export default function PlayPage() { + return ( + + + + ); +} diff --git a/src/components/EpisodeSelector.tsx b/src/components/EpisodeSelector.tsx new file mode 100644 index 0000000..e36ba3f --- /dev/null +++ b/src/components/EpisodeSelector.tsx @@ -0,0 +1,173 @@ +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; + +interface EpisodeSelectorProps { + /** 总集数 */ + totalEpisodes: number; + /** 每页显示多少集,默认 50 */ + episodesPerPage?: number; + /** 当前选中的集数(1 开始) */ + value?: number; + /** 用户点击选集后的回调 */ + onChange?: (episodeNumber: number) => void; + /** 额外 className */ + className?: string; +} + +/** + * 选集组件,支持分页与自动滚动聚焦当前分页标签。 + */ +const EpisodeSelector: React.FC = ({ + totalEpisodes, + episodesPerPage = 50, + value = 1, + onChange, + className = '', +}) => { + const pageCount = Math.ceil(totalEpisodes / episodesPerPage); + + // 当前分页索引(0 开始) + const initialPage = Math.floor((value - 1) / episodesPerPage); + const [currentPage, setCurrentPage] = useState(initialPage); + + // 是否倒序显示 + const [descending, setDescending] = useState(false); + + // 升序分页标签 + const categoriesAsc = useMemo(() => { + return Array.from({ length: pageCount }, (_, i) => { + const start = i * episodesPerPage + 1; + const end = Math.min(start + episodesPerPage - 1, totalEpisodes); + return `${start}-${end}`; + }); + }, [pageCount, episodesPerPage, totalEpisodes]); + + // 分页标签始终保持升序 + const categories = categoriesAsc; + + const categoryContainerRef = useRef(null); + const buttonRefs = useRef<(HTMLButtonElement | null)[]>([]); + + // 当分页切换时,将激活的分页标签滚动到视口中间 + useEffect(() => { + const btn = buttonRefs.current[currentPage]; + if (btn) { + btn.scrollIntoView({ + behavior: 'smooth', + inline: 'center', + block: 'nearest', + }); + } + }, [currentPage, pageCount]); + + const handleCategoryClick = useCallback((index: number) => { + setCurrentPage(index); + }, []); + + const handleEpisodeClick = useCallback( + (episodeNumber: number) => { + onChange?.(episodeNumber); + }, + [onChange] + ); + + const currentStart = currentPage * episodesPerPage + 1; + const currentEnd = Math.min( + currentStart + episodesPerPage - 1, + totalEpisodes + ); + + return ( +
+ {/* 分类标签 */} +
+
+
+ {categories.map((label, idx) => { + const isActive = idx === currentPage; + return ( + + ); + })} +
+
+ {/* 向上/向下按钮占位,可根据实际需求添加功能 */} + +
+ + {/* 集数网格 */} +
+ {(() => { + const len = currentEnd - currentStart + 1; + const episodes = Array.from({ length: len }, (_, i) => + descending ? currentEnd - i : currentStart + i + ); + return episodes; + })().map((episodeNumber) => { + const isActive = episodeNumber === value; + return ( + + ); + })} +
+
+ ); +}; + +export default EpisodeSelector;