7272 </div >
7373 </div >
7474 </div >
75+
76+ <!-- 选集列表弹窗 -->
77+ <div v-if =" showEpisodeDialog" class =" episode-dialog" >
78+ <div class =" episode-dialog-overlay" @click =" closeEpisodeDialog" ></div >
79+ <div class =" episode-dialog-content" >
80+ <div class =" episode-dialog-header" >
81+ <h3 >选择集数</h3 >
82+ <button @click =" closeEpisodeDialog" class =" episode-close-btn" >×</button >
83+ </div >
84+ <div ref =" episodeListRef" class =" episode-list" >
85+ <button
86+ v-for =" (episode, index) in props.episodes"
87+ :key =" index"
88+ :ref =" el => { if (index === props.currentEpisodeIndex) currentEpisodeRef = el }"
89+ @click =" selectEpisode(episode)"
90+ :class =" ['episode-item', { 'current': index === props.currentEpisodeIndex }]"
91+ >
92+ <span class =" episode-number" >{{ index + 1 }}</span >
93+ <span class =" episode-name" >{{ episode.name || `第${index + 1}集` }}</span >
94+ </button >
95+ </div >
96+ </div >
97+ </div >
7598 </div >
7699 </a-card >
77100</template >
@@ -121,7 +144,7 @@ const props = defineProps({
121144})
122145
123146// Emits
124- const emit = defineEmits ([' close' , ' error' , ' player-change' , ' next-episode' ])
147+ const emit = defineEmits ([' close' , ' error' , ' player-change' , ' next-episode' , ' episode-selected ' ])
125148
126149// 响应式数据
127150const artPlayerContainer = ref (null )
@@ -138,6 +161,11 @@ const autoNextTimer = ref(null) // 自动下一集定时器
138161const showAutoNextDialog = ref (false ) // 显示自动下一集对话框
139162const countdownEnabled = ref (false ) // 倒计时开关,默认关闭
140163
164+ // 选集弹窗相关数据
165+ const showEpisodeDialog = ref (false ) // 显示选集弹窗
166+ const episodeListRef = ref (null ) // 选集列表容器引用
167+ const currentEpisodeRef = ref (null ) // 当前选集按钮引用
168+
141169// 链接类型判断函数
142170const isDirectVideoLink = (url ) => {
143171 if (! url) return false
@@ -322,6 +350,15 @@ const initArtPlayer = async (url) => {
322350 playNextEpisode ()
323351 },
324352 },
353+ {
354+ position: ' right' ,
355+ html: props .episodes .length > 1 ? ' 选集' : ' ' ,
356+ tooltip: props .episodes .length > 1 ? ' 选择集数' : ' ' ,
357+ style: props .episodes .length > 1 ? {} : { display: ' none' },
358+ click : function () {
359+ toggleEpisodeDialog ()
360+ },
361+ },
325362 {
326363 position: ' right' ,
327364 html: ' 关闭' ,
@@ -603,6 +640,79 @@ const toggleCountdown = () => {
603640 }
604641}
605642
643+ // 滚动到当前选集位置
644+ const scrollToCurrentEpisode = async () => {
645+ // 等待DOM更新
646+ await nextTick ()
647+
648+ if (! episodeListRef .value || props .currentEpisodeIndex < 0 ) {
649+ return
650+ }
651+
652+ // 查找当前选集按钮
653+ const currentButton = episodeListRef .value .querySelector (' .episode-item.current' )
654+ if (! currentButton) {
655+ return
656+ }
657+
658+ const container = episodeListRef .value
659+ const containerHeight = container .clientHeight
660+ const containerScrollHeight = container .scrollHeight
661+ const buttonTop = currentButton .offsetTop
662+ const buttonHeight = currentButton .offsetHeight
663+
664+ // 计算滚动位置,让当前选集出现在容器的中间偏上位置(约30%处)
665+ const targetPosition = buttonTop + (buttonHeight / 2 ) - (containerHeight * 0.3 )
666+
667+ // 确保滚动位置在有效范围内
668+ const maxScrollTop = containerScrollHeight - containerHeight
669+ const targetScrollTop = Math .max (0 , Math .min (targetPosition, maxScrollTop))
670+
671+ // 只有当需要滚动的距离超过一定阈值时才执行滚动
672+ const currentScrollTop = container .scrollTop
673+ const scrollDistance = Math .abs (targetScrollTop - currentScrollTop)
674+
675+ if (scrollDistance > 50 ) { // 滚动距离超过50px才执行
676+ container .scrollTo ({
677+ top: targetScrollTop,
678+ behavior: ' smooth'
679+ })
680+ console .log (` 自动滚动到当前选集: 第${ props .currentEpisodeIndex + 1 } 集,滚动距离: ${ scrollDistance} px` )
681+ } else {
682+ console .log (` 当前选集已在可视区域中心,无需滚动: 第${ props .currentEpisodeIndex + 1 } 集` )
683+ }
684+ }
685+
686+ // 切换选集弹窗显示状态
687+ const toggleEpisodeDialog = async () => {
688+ showEpisodeDialog .value = ! showEpisodeDialog .value
689+ console .log (' 选集弹窗:' , showEpisodeDialog .value ? ' 显示' : ' 隐藏' )
690+
691+ // 如果弹窗打开,等待弹窗动画完成后再滚动
692+ if (showEpisodeDialog .value ) {
693+ // 延迟350ms,等待弹窗动画完成(CSS动画时长为300ms)
694+ setTimeout (async () => {
695+ await scrollToCurrentEpisode ()
696+ }, 350 )
697+ }
698+ }
699+
700+ // 关闭选集弹窗
701+ const closeEpisodeDialog = () => {
702+ showEpisodeDialog .value = false
703+ }
704+
705+ // 选择剧集
706+ const selectEpisode = (episode ) => {
707+ console .log (' 选择剧集:' , episode)
708+
709+ // 关闭弹窗
710+ closeEpisodeDialog ()
711+
712+ // 发送选集事件给父组件
713+ emit (' episode-selected' , episode)
714+ }
715+
606716// 监听视频URL变化
607717watch (() => props .videoUrl , async (newUrl ) => {
608718 if (newUrl && props .visible ) {
@@ -929,4 +1039,142 @@ onUnmounted(() => {
9291039.btn-cancel :hover {
9301040 background : #555 ;
9311041}
1042+
1043+ /* 选集弹窗样式 */
1044+ .episode-dialog {
1045+ position : fixed ;
1046+ top : 0 ;
1047+ left : 0 ;
1048+ width : 100% ;
1049+ height : 100% ;
1050+ z-index : 10000 ;
1051+ display : flex ;
1052+ align-items : center ;
1053+ justify-content : center ;
1054+ }
1055+
1056+ .episode-dialog-overlay {
1057+ position : absolute ;
1058+ top : 0 ;
1059+ left : 0 ;
1060+ width : 100% ;
1061+ height : 100% ;
1062+ background : rgba (0 , 0 , 0 , 0.7 );
1063+ backdrop-filter : blur (4px );
1064+ }
1065+
1066+ .episode-dialog-content {
1067+ position : relative ;
1068+ background : white ;
1069+ border-radius : 12px ;
1070+ box-shadow : 0 20px 40px rgba (0 , 0 , 0 , 0.3 );
1071+ max-width : 600px ;
1072+ max-height : 80vh ;
1073+ width : 90% ;
1074+ overflow : hidden ;
1075+ animation : episodeDialogShow 0.3s ease-out ;
1076+ }
1077+
1078+ @keyframes episodeDialogShow {
1079+ from {
1080+ opacity : 0 ;
1081+ transform : scale (0.9 ) translateY (-20px );
1082+ }
1083+ to {
1084+ opacity : 1 ;
1085+ transform : scale (1 ) translateY (0 );
1086+ }
1087+ }
1088+
1089+ .episode-dialog-header {
1090+ display : flex ;
1091+ justify-content : space-between ;
1092+ align-items : center ;
1093+ padding : 20px 24px ;
1094+ border-bottom : 1px solid #e8e8e8 ;
1095+ background : linear-gradient (135deg , #f5f7fa 0% , #c3cfe2 100% );
1096+ }
1097+
1098+ .episode-dialog-header h3 {
1099+ margin : 0 ;
1100+ font-size : 18px ;
1101+ font-weight : 600 ;
1102+ color : #2c3e50 ;
1103+ }
1104+
1105+ .episode-close-btn {
1106+ background : none ;
1107+ border : none ;
1108+ font-size : 24px ;
1109+ cursor : pointer ;
1110+ color : #666 ;
1111+ width : 32px ;
1112+ height : 32px ;
1113+ display : flex ;
1114+ align-items : center ;
1115+ justify-content : center ;
1116+ border-radius : 50% ;
1117+ transition : all 0.2s ease ;
1118+ }
1119+
1120+ .episode-close-btn :hover {
1121+ background : rgba (0 , 0 , 0 , 0.1 );
1122+ color : #333 ;
1123+ }
1124+
1125+ .episode-list {
1126+ padding : 16px ;
1127+ max-height : 60vh ;
1128+ overflow-y : auto ;
1129+ display : grid ;
1130+ grid-template-columns : repeat (auto-fill , minmax (200px , 1fr ));
1131+ gap : 12px ;
1132+ }
1133+
1134+ .episode-item {
1135+ display : flex ;
1136+ align-items : center ;
1137+ padding : 12px 16px ;
1138+ border : 2px solid #e8e8e8 ;
1139+ border-radius : 8px ;
1140+ background : white ;
1141+ cursor : pointer ;
1142+ transition : all 0.2s ease ;
1143+ text-align : left ;
1144+ min-height : 60px ;
1145+ }
1146+
1147+ .episode-item :hover {
1148+ border-color : #23ade5 ;
1149+ background : #f8fcff ;
1150+ transform : translateY (-2px );
1151+ box-shadow : 0 4px 12px rgba (35 , 173 , 229 , 0.2 );
1152+ }
1153+
1154+ .episode-item.current {
1155+ border-color : #23ade5 ;
1156+ background : linear-gradient (135deg , #23ade5 0% , #1e90ff 100% );
1157+ color : white ;
1158+ box-shadow : 0 4px 12px rgba (35 , 173 , 229 , 0.3 );
1159+ }
1160+
1161+ .episode-item.current :hover {
1162+ background : linear-gradient (135deg , #1e90ff 0% , #23ade5 100% );
1163+ }
1164+
1165+ .episode-number {
1166+ font-size : 16px ;
1167+ font-weight : bold ;
1168+ margin-right : 12px ;
1169+ min-width : 24px ;
1170+ text-align : center ;
1171+ }
1172+
1173+ .episode-name {
1174+ font-size : 14px ;
1175+ flex : 1 ;
1176+ overflow : hidden ;
1177+ text-overflow : ellipsis ;
1178+ white-space : nowrap ;
1179+ }
9321180 </style >
0 commit comments