VideoPlayer.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755
  1. <template>
  2. <div class="video-container">
  3. <div class="box-video">
  4. <!-- 视频 -->
  5. <template v-if="contentType === 'video'">
  6. <div class="video-wrapper" :class="{ 'video-paused': isVideoPaused }">
  7. <video
  8. class="full-box-video"
  9. ref="videoRef"
  10. :controls="true"
  11. :controlsList="'nofullscreen'"
  12. @timeupdate="handleTimeUpdate"
  13. @seeked="handleSeeked"
  14. @ended="handleVideoEnded"
  15. @loadedmetadata="handleLoadedMetadata"
  16. @pause="handleVideoPause"
  17. @play="handleVideoPlay"
  18. @click="handleVideoClick"
  19. ></video>
  20. <!-- 历史播放位置提示 -->
  21. <div
  22. v-if="showHistoryTip"
  23. class="history-tip"
  24. :style="{ left: historyTipPosition + '%' }"
  25. >
  26. <el-icon @click.stop="closeHistoryTip" class="close-icon"><CloseBold /></el-icon>
  27. <span @click="goToLastPosition">回到上次播放位置</span>
  28. </div>
  29. <!-- 章节要点小圆点 -->
  30. <div class="progress-markers" v-if="chapterMarkers.length > 0">
  31. <div
  32. v-for="marker in chapterMarkers"
  33. :key="marker.time"
  34. class="progress-marker"
  35. :style="{ left: marker.position + '%' }"
  36. @click="seekToTime(marker.time)"
  37. ></div>
  38. </div>
  39. <!-- 网页全屏按钮 -->
  40. <el-tooltip
  41. :content="isWebFullscreen ? '退出网页全屏' : '进入网页全屏'"
  42. placement="left"
  43. effect="dark"
  44. >
  45. <div class="fullscreen-btn" @click="toggleWebFullscreen">
  46. <el-icon v-if="!isWebFullscreen"><FullScreen /></el-icon>
  47. <el-icon v-else><Close /></el-icon>
  48. </div>
  49. </el-tooltip>
  50. </div>
  51. </template>
  52. </div>
  53. </div>
  54. </template>
  55. <script setup>
  56. import {
  57. ref,
  58. onMounted,
  59. onBeforeUnmount,
  60. defineProps,
  61. defineEmits,
  62. watch,
  63. nextTick
  64. } from 'vue'
  65. import { videoPlay as Vue3VideoPlay } from 'vue3-video-play'
  66. import Hls from 'hls.js'
  67. import { ElMessage } from 'element-plus'
  68. // 导入图标
  69. import { CloseBold, FullScreen, Close } from '@element-plus/icons-vue'
  70. // 定义props
  71. const props = defineProps({
  72. contentType: { type: String, required: true }, // contentType类型
  73. videoPath: { type: String }, // 变为可选
  74. courseId: { type: String, required: true },
  75. typeId: { type: String, required: true },
  76. courseConfigList: { type: Array, default: () => [] },
  77. allIndices: { type: Array, default: () => [] },
  78. currentIndex: { type: String, required: true }
  79. })
  80. // 定义emits
  81. const emits = defineEmits(['timeUpdate', 'videoEnded', 'switchVideo', 'saveProgress'])
  82. // 视频引用
  83. const videoRef = ref(null)
  84. // HLS实例
  85. const hlsRef = ref(null)
  86. // 记录已经暂停过的时间点索引
  87. const pausedIndices = ref([])
  88. // 记录已经保存的进度百分比
  89. const savedProgress = ref([])
  90. // 节流时间间隔(毫秒)
  91. const THROTTLE_TIME = 3000
  92. // 上次播放进度
  93. const lastPlayProgress = ref(0)
  94. // 定义进度数组
  95. const targetProgresses = [10, 50, 100]
  96. // 章节要点标记
  97. const chapterMarkers = ref([])
  98. // 历史播放位置提示状态
  99. const showHistoryTip = ref(false)
  100. // 上次播放位置时间
  101. const lastPlayTime = ref(0)
  102. // 历史播放提示位置百分比
  103. const historyTipPosition = ref(0)
  104. // 视频是否暂停状态
  105. const isVideoPaused = ref(false)
  106. // 是否处于网页全屏状态
  107. const isWebFullscreen = ref(false)
  108. // 定义节流函数
  109. const throttle = (fn, delay) => {
  110. let lastCall = 0
  111. return function (...args) {
  112. const now = Date.now()
  113. if (now - lastCall >= delay) {
  114. lastCall = now
  115. return fn.apply(this, args)
  116. }
  117. }
  118. }
  119. // 保存进度(带节流)
  120. const saveProgress = throttle(async (progress, currentTime) => {
  121. try {
  122. // 保存到localStorage,下次加载视频续播
  123. localStorage.setItem(
  124. `videoProgress_${props.courseId}`,
  125. JSON.stringify({
  126. progress: progress,
  127. currentTime: currentTime,
  128. timestamp: Date.now()
  129. })
  130. )
  131. // 通过emit事件通知父组件保存进度
  132. emits('saveProgress', "course", progress)
  133. savedProgress.value.push(progress)
  134. } catch (error) {
  135. console.error(`保存进度失败:`, error)
  136. }
  137. }, THROTTLE_TIME)
  138. // 处理视频元数据加载完成
  139. const handleLoadedMetadata = () => {
  140. if (!videoRef.value) return
  141. const duration = videoRef.value.duration
  142. if (duration && props.courseConfigList && props.courseConfigList.length) {
  143. // 根据courseConfigList生成章节标记,过滤掉ccTime不存在或无效的配置项
  144. chapterMarkers.value = props.courseConfigList
  145. .filter(config => config.ccTime !== undefined && config.ccTime !== null && config.ccTime !== '')
  146. .map(config => ({
  147. time: config.ccTime,
  148. position: (config.ccTime / duration) * 100
  149. }))
  150. } else {
  151. // 确保在courseConfigList为空时清空标记
  152. chapterMarkers.value = []
  153. }
  154. }
  155. // 跳转到指定时间点
  156. const seekToTime = (time) => {
  157. if (videoRef.value) {
  158. videoRef.value.currentTime = time
  159. // 清除暂停索引,用户点击后可以再次触发暂停
  160. pausedIndices.value = pausedIndices.value.filter(t => t !== time)
  161. }
  162. }
  163. // 处理视频时间更新事件
  164. const handleTimeUpdate = ev => {
  165. if (!videoRef.value) return
  166. const currentTime = parseInt(ev.target.currentTime)
  167. const duration = videoRef.value.duration || 0
  168. // 如果章节标记为空且有课程配置,生成标记
  169. if (chapterMarkers.value.length === 0 && props.courseConfigList.length && duration > 0) {
  170. handleLoadedMetadata()
  171. }
  172. const progressPercentage =
  173. duration > 0 ? Math.round((currentTime / duration) * 100) : 0
  174. // 更新最后播放进度
  175. lastPlayProgress.value = progressPercentage
  176. // 检查是否达到目标进度点且尚未保存
  177. targetProgresses.some(target => {
  178. const isNearTarget = Math.abs(progressPercentage - target) <= 2
  179. const isNotSaved = !savedProgress.value.includes(target)
  180. if (isNearTarget && isNotSaved) {
  181. // 保存目标进度
  182. saveProgress(target, currentTime)
  183. return true
  184. }
  185. return false
  186. })
  187. // 使用节流保存进度
  188. saveProgress(progressPercentage, currentTime)
  189. // 触发父组件的时间更新事件
  190. emits('timeUpdate', { currentTime, progressPercentage })
  191. if (!props.courseConfigList.length) return
  192. props.courseConfigList.forEach(courseCofig => {
  193. // 暂停时间
  194. let time = courseCofig.ccTime
  195. // 检查是否到达时间点且还未暂停过
  196. if (currentTime === time && !pausedIndices.value.includes(time)) {
  197. videoRef.value.pause()
  198. // 记录暂停时间
  199. pausedIndices.value.push(currentTime)
  200. // 根据 ccQuestSource 的值决定是否显示问题弹框
  201. // 当 ccQuestSource 为 1 时,显示问题弹框
  202. if (courseCofig.ccQuestSource === '1' && courseCofig.ccQuestContent) {
  203. // 触发父组件显示试题
  204. emits('timeUpdate', {
  205. currentTime,
  206. progressPercentage,
  207. courseConfig: courseCofig
  208. })
  209. }
  210. }
  211. })
  212. }
  213. // 视频完成拖动进度条时触发的方法
  214. const handleSeeked = () => {
  215. pausedIndices.value = []
  216. }
  217. // 视频结束事件处理
  218. const handleVideoEnded = () => {
  219. // 视频结束时保存100%进度
  220. if (!savedProgress.value.includes(100)) {
  221. // 通过emit事件通知父组件保存进度
  222. emits('saveProgress', "course", 100)
  223. savedProgress.value.push(100)
  224. }
  225. emits('videoEnded')
  226. }
  227. // 在视频加载完成后设置上次播放进度
  228. const setLastPlayPosition = () => {
  229. if (!videoRef.value) return
  230. try {
  231. const savedData = localStorage.getItem(`videoProgress_${props.courseId}`)
  232. if (savedData) {
  233. const { currentTime, progress } = JSON.parse(savedData)
  234. if (currentTime && !isNaN(currentTime) && currentTime > 0) {
  235. // 存储上次播放位置但不自动跳转
  236. lastPlayTime.value = currentTime
  237. lastPlayProgress.value = progress
  238. // 计算历史提示位置
  239. const duration = videoRef.value.duration
  240. historyTipPosition.value = duration > 0 ? (currentTime / duration) * 100 : 0
  241. showHistoryTip.value = true
  242. // 检查是否已有保存的进度点
  243. if (progress >= 10) savedProgress.value.push(10)
  244. if (progress >= 50) savedProgress.value.push(50)
  245. if (progress >= 100) savedProgress.value.push(100)
  246. // 5秒后自动隐藏历史位置提示
  247. setTimeout(() => {
  248. showHistoryTip.value = false
  249. }, 5000)
  250. }
  251. }
  252. } catch (error) {
  253. console.error('读取上次播放进度失败:', error)
  254. }
  255. }
  256. // 跳转到上次播放位置
  257. const goToLastPosition = () => {
  258. if (videoRef.value && lastPlayTime.value > 0) {
  259. videoRef.value.currentTime = lastPlayTime.value
  260. showHistoryTip.value = false
  261. // 自动播放视频
  262. videoRef.value.play().catch(error => {
  263. console.error('自动播放失败:', error)
  264. })
  265. }
  266. }
  267. // 关闭历史播放位置提示
  268. const closeHistoryTip = () => {
  269. showHistoryTip.value = false
  270. }
  271. // 处理视频暂停事件
  272. const handleVideoPause = () => {
  273. isVideoPaused.value = true
  274. }
  275. // 处理视频播放事件
  276. const handleVideoPlay = () => {
  277. isVideoPaused.value = false
  278. }
  279. // 处理视频点击事件
  280. const handleVideoClick = () => {
  281. if (videoRef.value) {
  282. if (videoRef.value.paused) {
  283. videoRef.value.play();
  284. } else {
  285. videoRef.value.pause();
  286. }
  287. }
  288. }
  289. // 初始化视频播放器
  290. const initVideoPlayer = () => {
  291. if (props.contentType !== 'video') return
  292. // 使用nextTick确保DOM已经更新
  293. nextTick(() => {
  294. if (!videoRef.value) {
  295. console.error('视频元素未找到')
  296. return
  297. }
  298. // 清理之前的HLS实例
  299. if (hlsRef.value) {
  300. hlsRef.value.destroy()
  301. hlsRef.value = null
  302. }
  303. // 为视频元素添加键盘事件监听器
  304. addVideoKeyboardListener();
  305. // 检查视频路径是否是m3u8格式
  306. if (props.videoPath && props.videoPath.toLowerCase().endsWith('.m3u8')) {
  307. // 使用HLS播放
  308. if (Hls.isSupported()) {
  309. hlsRef.value = new Hls()
  310. hlsRef.value.loadSource(props.videoPath)
  311. hlsRef.value.attachMedia(videoRef.value)
  312. hlsRef.value.on(Hls.Events.MANIFEST_PARSED, () => {
  313. tryPlayVideo()
  314. })
  315. hlsRef.value.on(Hls.Events.ERROR, (event, data) => {
  316. console.error('HLS错误:', data)
  317. // 只对致命错误显示错误提示
  318. if (data.fatal) {
  319. ElMessage.error('视频加载失败,请稍后重试')
  320. }
  321. })
  322. } else if (videoRef.value.canPlayType('application/vnd.apple.mpegurl')) {
  323. // 对于不支持HLS但支持原生m3u8的浏览器
  324. videoRef.value.src = props.videoPath
  325. tryPlayVideo()
  326. } else {
  327. ElMessage.error('您的浏览器不支持播放m3u8格式视频')
  328. }
  329. } else {
  330. // 普通视频播放
  331. videoRef.value.src = props.videoPath
  332. tryPlayVideo()
  333. }
  334. })
  335. }
  336. // 尝试播放视频,处理浏览器自动播放限制
  337. const tryPlayVideo = () => {
  338. // 确保videoRef存在
  339. if (!videoRef.value) {
  340. console.error('视频元素未找到')
  341. return
  342. }
  343. // 自动播放视频
  344. videoRef.value.play().catch(error => {
  345. console.error('自动播放失败:', error)
  346. })
  347. // 在视频加载完成后设置上次播放进度
  348. setTimeout(() => {
  349. setLastPlayPosition()
  350. }, 1000)
  351. }
  352. // 处理空格键控制播放/暂停
  353. const handleKeyPress = (event) => {
  354. // 检查是否是空格键且视频元素存在
  355. if (event.code === 'Space' && videoRef.value) {
  356. event.preventDefault(); // 防止空格键默认行为
  357. if (videoRef.value.paused) {
  358. videoRef.value.play();
  359. } else {
  360. videoRef.value.pause();
  361. }
  362. }
  363. }
  364. // 为视频元素添加键盘事件监听器
  365. const addVideoKeyboardListener = () => {
  366. if (videoRef.value) {
  367. videoRef.value.addEventListener('keydown', handleKeyPress);
  368. }
  369. }
  370. // 移除视频元素的键盘事件监听器
  371. const removeVideoKeyboardListener = () => {
  372. if (videoRef.value) {
  373. videoRef.value.removeEventListener('keydown', handleKeyPress);
  374. }
  375. }
  376. // 组件挂载时
  377. onMounted(() => {
  378. initVideoPlayer()
  379. // 键盘事件监听
  380. document.addEventListener('keydown', handleKeyPress);
  381. // 为视频元素添加键盘事件监听器
  382. addVideoKeyboardListener();
  383. })
  384. // 组件卸载时
  385. onBeforeUnmount(() => {
  386. if (hlsRef.value) {
  387. hlsRef.value.destroy()
  388. hlsRef.value = null
  389. }
  390. // 移除键盘事件监听
  391. document.removeEventListener('keydown', handleKeyPress);
  392. // 移除视频元素的键盘事件监听器
  393. removeVideoKeyboardListener();
  394. })
  395. // 监听contentType和videoPath变化
  396. watch([() => props.contentType, () => props.videoPath], () => {
  397. // 当contentType变为video或videoPath变化时,重新初始化
  398. if (props.contentType === 'video') {
  399. initVideoPlayer()
  400. }
  401. })
  402. // 切换网页全屏
  403. const toggleWebFullscreen = () => {
  404. const videoWrapper = document.querySelector('.video-wrapper')
  405. if (!videoWrapper) return
  406. if (!isWebFullscreen.value) {
  407. // 进入网页全屏
  408. videoWrapper.style.position = 'fixed'
  409. videoWrapper.style.top = '0'
  410. videoWrapper.style.left = '0'
  411. videoWrapper.style.width = '100vw'
  412. videoWrapper.style.height = '100vh'
  413. videoWrapper.style.zIndex = '9999'
  414. videoWrapper.style.backgroundColor = 'black'
  415. videoWrapper.style.display = 'flex'
  416. videoWrapper.style.justifyContent = 'center'
  417. videoWrapper.style.alignItems = 'center'
  418. // 调整视频大小
  419. const video = videoWrapper.querySelector('.full-box-video')
  420. if (video) {
  421. video.style.width = '100%'
  422. video.style.height = '100%'
  423. video.style.objectFit = 'contain'
  424. }
  425. isWebFullscreen.value = true
  426. } else {
  427. // 退出网页全屏
  428. videoWrapper.style.position = ''
  429. videoWrapper.style.top = ''
  430. videoWrapper.style.left = ''
  431. videoWrapper.style.width = '70%'
  432. videoWrapper.style.height = '100%'
  433. videoWrapper.style.zIndex = ''
  434. videoWrapper.style.backgroundColor = ''
  435. videoWrapper.style.display = 'flex'
  436. videoWrapper.style.justifyContent = 'center'
  437. videoWrapper.style.alignItems = 'center'
  438. // 恢复视频大小
  439. const video = videoWrapper.querySelector('.full-box-video')
  440. if (video) {
  441. video.style.width = '100%'
  442. video.style.height = '100%'
  443. video.style.objectFit = 'cover'
  444. }
  445. isWebFullscreen.value = false
  446. }
  447. }
  448. // 组件卸载时
  449. onBeforeUnmount(() => {
  450. if (hlsRef.value) {
  451. hlsRef.value.destroy()
  452. hlsRef.value = null
  453. }
  454. // 确保退出网页全屏
  455. if (isWebFullscreen.value) {
  456. const videoWrapper = document.querySelector('.video-wrapper')
  457. if (videoWrapper) {
  458. videoWrapper.style.position = ''
  459. videoWrapper.style.top = ''
  460. videoWrapper.style.left = ''
  461. videoWrapper.style.width = '70%'
  462. videoWrapper.style.height = '100%'
  463. videoWrapper.style.zIndex = ''
  464. videoWrapper.style.backgroundColor = ''
  465. }
  466. }
  467. // 移除键盘事件监听
  468. document.removeEventListener('keydown', handleKeyPress);
  469. // 移除视频元素的键盘事件监听器
  470. removeVideoKeyboardListener();
  471. })
  472. </script>
  473. <style scoped lang="scss">
  474. @use 'sass:math';
  475. // 定义rpx转换函数
  476. @function rpx($px) {
  477. @return math.div($px, 750) * 100vw;
  478. }
  479. .box-video {
  480. width: 100%;
  481. height: rpx(300);
  482. display: flex;
  483. justify-content: center;
  484. align-items: center;
  485. .d-player-wrap {
  486. height: rpx(289);
  487. width: 68.5%;
  488. border-radius: rpx(12);
  489. object-fit: cover;
  490. }
  491. }
  492. .video-wrapper {
  493. position: relative;
  494. width: 70%;
  495. height: 100%;
  496. display: flex;
  497. justify-content: center;
  498. align-items: center;
  499. }
  500. .full-box-video {
  501. width: 100%;
  502. height: 100%;
  503. object-fit: cover;
  504. border-radius: rpx(12);
  505. }
  506. .ppt-box{
  507. width: 100%;
  508. height: rpx(300);
  509. display: flex;
  510. justify-content: center;
  511. align-items: center;
  512. position: relative;
  513. }
  514. .ppt-page-info {
  515. position: absolute;
  516. bottom: rpx(10); // 距离底部10rpx
  517. left: 50%; // 水平居中
  518. transform: translateX(-50%); // 水平居中调整
  519. background-color: rgba(0, 0, 0, 0.5);
  520. color: white;
  521. padding: rpx(3) rpx(8);
  522. border-radius: rpx(4);
  523. font-size: rpx(8);
  524. z-index: 10;
  525. }
  526. .ppt-container {
  527. width: 70%;
  528. height: 100%;
  529. border-radius: rpx(12);
  530. overflow: hidden;
  531. }
  532. .ppt-container ::v-deep(.pptx-preview-wrapper) {
  533. // 滚动条整体样式
  534. &::-webkit-scrollbar {
  535. width: rpx(0); // 滚动条宽度
  536. height: rpx(0);
  537. }
  538. // 滚动条滑块样式
  539. &::-webkit-scrollbar-thumb {
  540. background-color: #e2ddfc; // 滑块颜色
  541. border-radius: rpx(4); // 滑块圆角
  542. }
  543. // 滚动条轨道样式
  544. &::-webkit-scrollbar-track {
  545. background-color: rgba(143, 116, 255, 0.2); // 轨道颜色
  546. border-radius: rpx(4); // 轨道圆角
  547. }
  548. // border-radius: rpx(12);
  549. }
  550. .ppt-navigation {
  551. position: absolute;
  552. bottom: rpx(18);
  553. width: 70%;
  554. display: flex;
  555. justify-content: center;
  556. gap: rpx(20);
  557. padding: 0 rpx(10);
  558. box-sizing: border-box;
  559. }
  560. .ppt-btn {
  561. background-color: rgb(255, 255, 255, 0.5);
  562. color: white;
  563. border: 1px white solid;
  564. border-radius: rpx(12);
  565. font-size: rpx(7);
  566. cursor: pointer;
  567. transition: background-color 0.3s;
  568. display: flex;
  569. align-items: center;
  570. justify-content: center;
  571. height: rpx(15);
  572. }
  573. .ppt-btn:hover {
  574. background-color: rgba(0, 0, 0, 0.7);
  575. }
  576. .ppt-prev-btn, .ppt-next-btn {
  577. width: rpx(50);
  578. }
  579. /* 章节要点标记样式 */
  580. .progress-markers {
  581. position: absolute;
  582. bottom: 26px;
  583. left: 15px;
  584. right: 15px;
  585. height: 10px;
  586. pointer-events: none;
  587. z-index: 10;
  588. opacity: 0; /* 默认隐藏 */
  589. transition: opacity 0.3s ease; /* 过渡效果 */
  590. }
  591. .video-wrapper:hover .progress-markers {
  592. opacity: 1; /* 鼠标悬停在视频上时显示 */
  593. }
  594. /* 当视频控件可见时显示标记 */
  595. video::-webkit-media-controls-panel:not([hidden]) ~ .progress-markers {
  596. opacity: 1;
  597. }
  598. /* 当视频暂停时显示标记 */
  599. .video-paused .progress-markers {
  600. opacity: 1;
  601. }
  602. .progress-marker {position: absolute;
  603. width: rpx(4);
  604. height: rpx(4);
  605. background-color: orange;
  606. border-radius: 50%;
  607. transform: translateX(-50%);
  608. cursor: pointer;
  609. transition: all 0.3s ease;
  610. pointer-events: all;
  611. }
  612. .progress-marker:hover {
  613. width: rpx(6);
  614. height: rpx(6);
  615. }
  616. /* 历史播放位置提示样式 */
  617. .history-tip {
  618. position: absolute;
  619. bottom: rpx(22);
  620. transform: translateX(-50%);
  621. background-color: rgba(0, 0, 0, 0.5);
  622. color: white;
  623. padding: rpx(5) rpx(7);
  624. border-radius: rpx(4);
  625. font-size: rpx(6);
  626. cursor: pointer;
  627. transition: all 0.3s ease;
  628. z-index: 20;
  629. white-space: nowrap;
  630. display: flex;
  631. align-items: center;
  632. gap: rpx(2);
  633. }
  634. .history-tip .close-icon {
  635. cursor: pointer;
  636. font-size: rpx(7);
  637. transition: color 0.3s ease;
  638. }
  639. .history-tip .close-icon:hover {
  640. color: #ff4d4f;
  641. }
  642. .history-tip::after {
  643. content: '';
  644. position: absolute;
  645. top: 100%;
  646. left: 50%;
  647. transform: translateX(-50%);
  648. border: rpx(4) solid transparent;
  649. border-top-color: rgba(0, 0, 0, 0.5);
  650. width: 0;
  651. height: 0;
  652. }
  653. .history-tip:hover {
  654. background-color: rgba(0, 0, 0, 0.6);
  655. }
  656. /* 网页全屏按钮样式 */
  657. .fullscreen-btn {
  658. position: absolute;
  659. top: 10px;
  660. right: 10px;
  661. width: 30px;
  662. height: 30px;
  663. background-color: rgba(0, 0, 0, 0.5);
  664. color: white;
  665. border-radius: 4px;
  666. display: flex;
  667. justify-content: center;
  668. align-items: center;
  669. cursor: pointer;
  670. z-index: 15;
  671. transition: all 0.3s ease;
  672. }
  673. .fullscreen-btn:hover {
  674. background-color: rgba(0, 0, 0, 0.7);
  675. }
  676. /* 调整视频控件样式以适应我们的自定义标记 */
  677. video::-webkit-media-controls-timeline {
  678. margin-bottom: 10px;
  679. }
  680. video::-webkit-media-controls {
  681. overflow: visible !important;
  682. }
  683. video::-webkit-media-controls-panel {
  684. width: calc(100% + 30px); /* 扩展控件面板宽度,确保与自定义标记对齐 */
  685. background: transparent !important; /* 去掉背景渐变,设为透明 */
  686. }
  687. </style>