Просмотр исходного кода

更新VideoJSPlayer组件,优化封面显示逻辑,确保在无视频源时正确显示封面,并添加快进10秒按钮以提升用户体验。同时,调整视频源传递逻辑,确保非付费用户无法访问视频内容。

wuyi 2 месяцев назад
Родитель
Сommit
76dd5452ee
3 измененных файлов с 194 добавлено и 487 удалено
  1. 1 1
      dev-dist/sw.js
  2. 159 453
      src/components/VideoJSPlayer.vue
  3. 34 33
      src/views/VideoPlayer.vue

+ 1 - 1
dev-dist/sw.js

@@ -79,7 +79,7 @@ define(['./workbox-f2cb1a81'], (function (workbox) { 'use strict';
    */
   workbox.precacheAndRoute([{
     "url": "index.html",
-    "revision": "0.vad8o11l318"
+    "revision": "0.t1ek31rf02g"
   }], {});
   workbox.cleanupOutdatedCaches();
   workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

+ 159 - 453
src/components/VideoJSPlayer.vue

@@ -1,24 +1,23 @@
 <template>
   <div class="video-js-container">
-    <!-- 封面图片 -->
+    <!-- 封面图片 - 只在没有视频源时显示 -->
     <img
+      v-if="!hasVideoSource && processedCoverUrl"
       :src="processedCoverUrl"
       :alt="alt"
       :class="coverClass"
       @error="handleCoverError"
       @load="handleCoverLoad"
       style="
-        display: block !important;
         width: 100% !important;
         height: 100% !important;
         opacity: 1 !important;
-        z-index: 999 !important;
       "
     />
 
-    <!-- Video.js 播放器容器 -->
+    <!-- Video.js 播放器容器 - 只在有视频源时显示 -->
     <div
-      v-if="isVideoMode || forceShowPlayer"
+      v-if="hasVideoSource"
       ref="videoContainer"
       class="video-js-wrapper"
       :class="[videoClass, { 'loading-video': loading }]"
@@ -94,16 +93,14 @@ const videoContainer = ref<HTMLDivElement>();
 const player = ref<any>(null);
 
 // 计算属性
-const isVideoMode = computed(
+const hasVideoSource = computed(
   () => !!props.m3u8Url && props.m3u8Url.trim() !== ""
 );
+
 const showRetryButton = computed(
   () => props.enableRetry && retryCount.value < maxRetries
 );
 
-// 强制显示播放器的状态
-const forceShowPlayer = ref(false);
-
 // 核心解密函数(仅用于封面)
 const loader = async (url: string): Promise<Blob | string> => {
   const response = await fetch(url, { mode: "cors" });
@@ -168,7 +165,6 @@ const processCover = async (url: string): Promise<void> => {
   try {
     loading.value = true;
     error.value = "";
-    console.log("处理封面URL:", url);
 
     // 检查是否需要解密(仅对封面进行解密)
     if (url.includes("cover")) {
@@ -179,10 +175,8 @@ const processCover = async (url: string): Promise<void> => {
           // 直接使用解密后的 Blob 创建 URL
           const blobUrl = URL.createObjectURL(decryptedData);
           processedCoverUrl.value = blobUrl;
-          console.log("封面解密成功,创建Blob URL:", blobUrl);
         } else {
           processedCoverUrl.value = decryptedData;
-          console.log("封面解密成功,使用解密后的URL");
         }
       } catch (decryptErr) {
         console.error("封面解密失败,使用原始URL:", decryptErr);
@@ -196,21 +190,6 @@ const processCover = async (url: string): Promise<void> => {
     // 预加载图片以确保它可以正确显示
     const img = new Image();
     img.onload = () => {
-      console.log("封面图片加载成功,URL:", processedCoverUrl.value);
-
-      // 如果不是视频模式,确保封面图片立即显示
-      if (!isVideoMode.value) {
-        nextTick(() => {
-          const imgElement = document.querySelector(
-            ".video-js-container img"
-          ) as HTMLImageElement;
-          if (imgElement) {
-            imgElement.style.display = "block";
-            imgElement.style.opacity = "1";
-          }
-        });
-      }
-
       emit("coverLoaded", processedCoverUrl.value);
     };
     img.onerror = () => {
@@ -239,12 +218,12 @@ const processVideo = async (url: string): Promise<void> => {
   processedVideoUrl.value = url;
   await nextTick();
   await initVideoJSPlayer();
-  emit("videoLoaded", processedVideoUrl.value);
+  emit("videoLoaded", url);
 };
 
 // 初始化 Video.js 播放器
 const initVideoJSPlayer = async (): Promise<void> => {
-  return new Promise((resolve, reject) => {
+  return new Promise(async (resolve, reject) => {
     if (!videoContainer.value || !processedVideoUrl.value) {
       console.log("初始化失败:缺少容器或视频URL");
       reject(new Error("缺少容器或视频URL"));
@@ -256,17 +235,12 @@ const initVideoJSPlayer = async (): Promise<void> => {
       videoContainer.value.innerHTML = "";
     }
 
-    console.log("开始初始化 Video.js 播放器");
-    console.log("视频URL:", processedVideoUrl.value);
-
-    // 先测试 URL 是否可访问
-    fetch(processedVideoUrl.value, { method: "HEAD" })
-      .then((response) => {
-        console.log("URL 可访问性测试:", response.status, response.ok);
-      })
-      .catch((error) => {
-        console.error("URL 访问测试失败:", error);
-      });
+    // 处理封面 URL
+    let posterUrl = "";
+    if (props.coverUrl) {
+      await processCover(props.coverUrl);
+      posterUrl = processedCoverUrl.value;
+    }
 
     // 创建 video 元素
     const videoElement = document.createElement("video");
@@ -274,13 +248,14 @@ const initVideoJSPlayer = async (): Promise<void> => {
     videoElement.controls = true;
     videoElement.preload = "auto";
 
-    // 确保设置封面
-    if (processedCoverUrl.value) {
-      videoElement.poster = processedCoverUrl.value;
-      console.log("设置视频封面:", processedCoverUrl.value);
-    } else if (props.coverUrl) {
-      videoElement.poster = props.coverUrl;
-      console.log("使用原始封面URL:", props.coverUrl);
+    // iOS 特殊配置,防止自动全屏
+    videoElement.setAttribute("playsinline", "true");
+    videoElement.setAttribute("webkit-playsinline", "true");
+    videoElement.setAttribute("x-webkit-airplay", "allow");
+
+    // 设置封面
+    if (posterUrl) {
+      videoElement.poster = posterUrl;
     }
 
     // 添加到容器
@@ -293,16 +268,20 @@ const initVideoJSPlayer = async (): Promise<void> => {
       preload: "auto",
       techOrder: ["html5"],
       bigPlayButton: true, // 启用大型播放按钮
+      // iOS 特殊配置
+      playsinline: true,
+      webkitPlaysinline: true,
       userActions: {
-        // 禁用默认的点击暂停行为,但允许点击进度条
-        click: false,
-        doubleClick: false,
+        // 保持默认的点击行为,让控制栏可以正常显示/隐藏
+        click: true,
+        doubleClick: true,
         hotkeys: true,
       },
       controlBar: {
         // 自定义控制栏
         children: [
           "playToggle", // 播放/暂停按钮
+          "forward10Button", // 快进 10 秒按钮
           "replayButton", // 回退按钮
           "forwardButton", // 快进按钮
           "currentTimeDisplay", // 当前时间
@@ -318,13 +297,15 @@ const initVideoJSPlayer = async (): Promise<void> => {
         },
       },
       // 使用默认的 HTML5 配置,Video.js 内置 HLS 支持
-      sources: [
-        {
-          src: processedVideoUrl.value,
-          type: "application/x-mpegURL",
-        },
-      ],
-      poster: processedCoverUrl.value || props.coverUrl,
+      sources: processedVideoUrl.value
+        ? [
+            {
+              src: processedVideoUrl.value,
+              type: "application/x-mpegURL",
+            },
+          ]
+        : [],
+      poster: posterUrl, // 使用处理后的封面URL
     };
 
     try {
@@ -375,6 +356,33 @@ const initVideoJSPlayer = async (): Promise<void> => {
             }
           }
           videojs.registerComponent("ForwardButton", ForwardButtonComponent);
+
+          // 注册快进 10 秒按钮
+          const Forward10Button = videojs.getComponent("Button");
+          class Forward10ButtonComponent extends Forward10Button {
+            constructor(player: any, options: any) {
+              super(player, options);
+              (this as any).controlText("快进10秒");
+            }
+
+            handleClick() {
+              const time = (this as any).player().currentTime();
+              const duration = (this as any).player().duration();
+              if (typeof time === "number" && typeof duration === "number") {
+                (this as any)
+                  .player()
+                  .currentTime(Math.min(duration, time + 10));
+              }
+            }
+
+            buildCSSClass() {
+              return `vjs-forward-10-button ${super.buildCSSClass()}`;
+            }
+          }
+          videojs.registerComponent(
+            "Forward10Button",
+            Forward10ButtonComponent
+          );
         } catch (err) {
           console.error("注册快进快退按钮失败:", err);
         }
@@ -382,67 +390,66 @@ const initVideoJSPlayer = async (): Promise<void> => {
 
       registerSeekButtons();
       player.value = videojs(videoElement, options);
-      console.log("Video.js 播放器创建成功");
 
       // 自定义点击行为
       player.value.ready(() => {
-        console.log("Video.js 播放器准备就绪");
         error.value = "";
         loading.value = false;
         emit("canplay");
 
-        // 禁用默认的点击暂停行为
-        player.value.tech_.off("tap");
-
-        // 添加自定义点击处理
-        const playerEl = player.value.el();
-        let clickTimeout: number | null = null;
-        let clickCount = 0;
-
-        playerEl.addEventListener("click", (event: MouseEvent) => {
-          // 忽略控制栏和进度条上的点击
-          const target = event.target as HTMLElement;
-          if (
-            target.closest(".vjs-control-bar") ||
-            target.closest(".vjs-big-play-button") ||
-            target.closest(".vjs-progress-control") ||
-            target.closest(".vjs-progress-holder") ||
-            target.closest(".vjs-play-progress") ||
-            target.closest(".vjs-load-progress") ||
-            target.classList.contains("vjs-progress-control") ||
-            target.classList.contains("vjs-progress-holder") ||
-            target.classList.contains("vjs-play-progress") ||
-            target.classList.contains("vjs-load-progress")
-          ) {
-            return;
-          }
+        // 使用 Video.js 默认的点击行为,不需要自定义处理
 
-          clickCount++;
-          console.log("点击计数:", clickCount);
-
-          if (clickCount === 1) {
-            clickTimeout = window.setTimeout(() => {
-              // 单击 - 只显示控制栏
-              console.log("单击 - 显示控制栏");
-              player.value.userActive(true);
-              clickCount = 0;
-              clickTimeout = null;
-            }, 300);
-          } else if (clickCount === 2) {
-            // 双击 - 暂停/播放
-            console.log("双击 - 切换播放/暂停状态");
-            if (clickTimeout) {
-              clearTimeout(clickTimeout);
-              clickTimeout = null;
-            }
+        // 确保 iOS 设备上的内联播放设置
+        const videoEl = player.value.el().querySelector("video");
+        if (videoEl) {
+          videoEl.setAttribute("playsinline", "true");
+          videoEl.setAttribute("webkit-playsinline", "true");
+          videoEl.setAttribute("x-webkit-airplay", "allow");
+        }
 
-            if (player.value.paused()) {
-              player.value.play();
-            } else {
-              player.value.pause();
-            }
+        // 监听播放和暂停事件,动态更新 poster
+        player.value.on("play", () => {
+          // 播放时隐藏 poster
+          if (player.value.poster()) {
+            player.value.poster("");
+          }
+        });
 
-            clickCount = 0;
+        player.value.on("pause", () => {
+          // 暂停时显示当前帧作为 poster
+          const videoElement = player.value.el().querySelector("video");
+          if (videoElement) {
+            // 创建 canvas 来捕获当前帧
+            const canvas = document.createElement("canvas");
+            const ctx = canvas.getContext("2d");
+
+            if (ctx) {
+              canvas.width = videoElement.videoWidth;
+              canvas.height = videoElement.videoHeight;
+              ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
+
+              // 将 canvas 转换为 blob URL
+              canvas.toBlob(
+                (blob) => {
+                  if (blob) {
+                    const currentFrameUrl = URL.createObjectURL(blob);
+                    player.value.poster(currentFrameUrl);
+
+                    // 清理之前的 blob URL
+                    const oldPoster = player.value.poster();
+                    if (
+                      oldPoster &&
+                      oldPoster.startsWith("blob:") &&
+                      oldPoster !== currentFrameUrl
+                    ) {
+                      URL.revokeObjectURL(oldPoster);
+                    }
+                  }
+                },
+                "image/jpeg",
+                0.8
+              );
+            }
           }
         });
 
@@ -450,24 +457,20 @@ const initVideoJSPlayer = async (): Promise<void> => {
       });
 
       player.value.on("loadstart", () => {
-        console.log("开始加载视频");
         loading.value = true;
       });
 
       player.value.on("loadeddata", () => {
-        console.log("视频数据加载完成");
         loading.value = false;
       });
 
       player.value.on("canplay", () => {
-        console.log("视频可以播放");
         loading.value = false;
         error.value = "";
         emit("canplay");
       });
 
       player.value.on("play", () => {
-        console.log("开始播放");
         emit("play");
       });
 
@@ -522,7 +525,6 @@ const stopVideo = (): void => {
     player.value.currentTime(0);
   }
   destroyPlayer();
-  forceShowPlayer.value = false;
 };
 
 // 事件处理
@@ -534,9 +536,8 @@ const handleCoverError = (event: Event): void => {
 };
 
 const handleCoverLoad = (): void => {
-  console.log("封面图片加载完成,显示封面");
   // 确保图片可见
-  if (!isVideoMode.value && !forceShowPlayer.value) {
+  if (!hasVideoSource.value) {
     const img = document.querySelector(
       ".video-js-container img"
     ) as HTMLImageElement;
@@ -582,6 +583,14 @@ watch(
 
 // 组件卸载时清理资源
 onUnmounted(() => {
+  // 清理播放器的 poster blob URL
+  if (player.value && player.value.poster()) {
+    const posterUrl = player.value.poster();
+    if (posterUrl.startsWith("blob:")) {
+      URL.revokeObjectURL(posterUrl);
+    }
+  }
+
   destroyPlayer();
 
   // 清理 Blob URL
@@ -594,146 +603,11 @@ onUnmounted(() => {
 const playVideo = (): void => {
   console.log("手动播放被调用");
 
-  // 强制显示播放器
-  forceShowPlayer.value = true;
-
-  // 先确保销毁现有播放器
-  destroyPlayer();
-
-  // 等待 DOM 更新后再初始化播放器
-  nextTick(async () => {
-    try {
-      // 重新初始化播放器
-      await initVideoJSPlayer();
-
-      // 确保播放器准备就绪后再播放
-      if (player.value) {
-        player.value.ready(() => {
-          console.log("播放器准备就绪,开始播放");
-
-          // 检查播放器状态
-          console.log("播放器技术:", player.value.techName_);
-          console.log("播放器源:", player.value.currentSource());
-
-          // 禁用默认的点击暂停行为
-          if (player.value.tech_) {
-            player.value.tech_.off("tap");
-
-            // 添加自定义点击处理
-            const playerEl = player.value.el();
-            let clickTimeout: number | null = null;
-            let clickCount = 0;
-
-            playerEl.addEventListener("click", (event: MouseEvent) => {
-              // 忽略控制栏和进度条上的点击
-              const target = event.target as HTMLElement;
-              if (
-                target.closest(".vjs-control-bar") ||
-                target.closest(".vjs-big-play-button") ||
-                target.closest(".vjs-progress-control") ||
-                target.closest(".vjs-progress-holder") ||
-                target.closest(".vjs-play-progress") ||
-                target.closest(".vjs-load-progress") ||
-                target.classList.contains("vjs-progress-control") ||
-                target.classList.contains("vjs-progress-holder") ||
-                target.classList.contains("vjs-play-progress") ||
-                target.classList.contains("vjs-load-progress")
-              ) {
-                return;
-              }
-
-              clickCount++;
-              console.log("点击计数:", clickCount);
-
-              if (clickCount === 1) {
-                clickTimeout = window.setTimeout(() => {
-                  // 单击 - 只显示控制栏
-                  console.log("单击 - 显示控制栏");
-                  player.value.userActive(true);
-                  clickCount = 0;
-                  clickTimeout = null;
-                }, 300);
-              } else if (clickCount === 2) {
-                // 双击 - 暂停/播放
-                console.log("双击 - 切换播放/暂停状态");
-                if (clickTimeout) {
-                  clearTimeout(clickTimeout);
-                  clickTimeout = null;
-                }
-
-                if (player.value.paused()) {
-                  player.value.play();
-                } else {
-                  player.value.pause();
-                }
-
-                clickCount = 0;
-              }
-            });
-          }
-
-          // 检查浏览器 HLS 支持
-          const testVideo = document.createElement("video");
-          console.log("浏览器 HLS 支持:", {
-            "application/x-mpegURL": testVideo.canPlayType(
-              "application/x-mpegURL"
-            ),
-            "application/vnd.apple.mpegurl": testVideo.canPlayType(
-              "application/vnd.apple.mpegurl"
-            ),
-          });
-
-          // 延迟一点时间再播放
-          setTimeout(() => {
-            try {
-              if (player.value) {
-                // 确保源已经设置
-                player.value.src(player.value.currentSource());
-
-                console.log("尝试播放...");
-
-                const playPromise = player.value.play();
-
-                if (playPromise !== undefined) {
-                  playPromise
-                    .then(() => {
-                      console.log("播放成功启动");
-                    })
-                    .catch((err: any) => {
-                      console.error("播放失败:", err);
-
-                      // 尝试使用原生 video 元素播放
-                      console.log("尝试使用原生 video 元素播放");
-                      const video = document.createElement("video");
-                      video.src = processedVideoUrl.value;
-                      video.controls = true;
-                      video.style.width = "100%";
-                      video.style.height = "100%";
-
-                      if (videoContainer.value) {
-                        // 清空容器并添加原生视频
-                        destroyPlayer();
-                        videoContainer.value.innerHTML = "";
-                        videoContainer.value.appendChild(video);
-
-                        // 尝试播放
-                        video
-                          .play()
-                          .catch((e) => console.error("原生播放也失败:", e));
-                      }
-                    });
-                }
-              }
-            } catch (e) {
-              console.error("播放时发生异常:", e);
-            }
-          }, 500);
-        });
-      }
-    } catch (err) {
-      console.error("初始化播放器失败:", err);
-    }
-  });
+  if (player.value) {
+    player.value.play().catch((err: any) => {
+      console.error("播放失败:", err);
+    });
+  }
 };
 
 // 暂停播放方法
@@ -747,7 +621,6 @@ defineExpose({
   stopVideo,
   playVideo,
   pauseVideo,
-  processedCoverUrl: computed(() => processedCoverUrl.value),
   processedVideoUrl: computed(() => processedVideoUrl.value),
   loading: computed(() => loading.value),
   error: computed(() => error.value),
@@ -764,7 +637,6 @@ defineExpose({
 }
 
 .video-js-container img {
-  display: block;
   width: 100%;
   height: 100%;
   object-fit: cover;
@@ -788,23 +660,25 @@ defineExpose({
   background-color: #000;
 }
 
-/* 确保封面图片始终显示 */
+/* Video.js poster 样式 */
 :deep(.video-js .vjs-poster) {
   background-size: cover;
   opacity: 1 !important;
-  display: block !important;
   background-position: center center;
   background-repeat: no-repeat;
   z-index: 1;
 }
 
-/* 确保封面图片在加载中状态下也显示 */
-:deep(.video-js.vjs-has-started .vjs-poster) {
-  display: none;
+/* 播放时隐藏 poster */
+:deep(.video-js.vjs-playing .vjs-poster) {
+  display: none !important;
 }
 
+/* 暂停时显示 poster */
 :deep(.video-js.vjs-paused .vjs-poster) {
   display: block !important;
+  opacity: 1 !important;
+  z-index: 1;
 }
 
 /* 自定义中央播放按钮样式 */
@@ -833,182 +707,40 @@ defineExpose({
   transform: scale(1.1);
 }
 
-:deep(.video-js .vjs-control-bar) {
-  background: linear-gradient(
-    to top,
-    rgba(0, 0, 0, 0.95) 0%,
-    rgba(0, 0, 0, 0.7) 60%,
-    rgba(0, 0, 0, 0) 100%
-  );
-  height: 4em; /* 增加控制栏高度 */
-  padding: 0 1.2em; /* 增加左右内边距 */
-  opacity: 1; /* 始终显示控制栏 */
-  transform: translateY(0);
-  transition: opacity 0.3s ease;
-}
-
-/* 确保控制栏在用户交互时始终可见 */
-:deep(.video-js.vjs-user-active .vjs-control-bar) {
-  opacity: 1;
-  visibility: visible;
-  pointer-events: auto;
-  animation: fadeIn 0.3s ease;
-}
-
-@keyframes fadeIn {
-  from {
-    opacity: 0;
-    transform: translateY(5px);
-  }
-  to {
-    opacity: 1;
-    transform: translateY(0);
-  }
-}
-
-/* 控制栏按钮样式 */
-:deep(.video-js .vjs-control) {
-  width: 3.2em;
-  opacity: 0.8;
-  transition: opacity 0.2s ease, transform 0.2s ease;
-}
-
-:deep(.video-js .vjs-control:hover) {
-  opacity: 1;
-  transform: scale(1.1);
-}
-
-/* 全屏按钮样式 */
-:deep(.video-js .vjs-fullscreen-control) {
-  position: absolute;
-  right: 0.5em;
-}
-
-/* 播放按钮样式 */
-:deep(.video-js .vjs-play-control) {
-  font-size: 1.3em;
-}
-
-/* 进度条样式 */
-:deep(.video-js .vjs-progress-control) {
-  position: absolute;
-  bottom: 4em;
-  left: 0;
-  right: 0;
-  height: 1em;
-  background: rgba(0, 0, 0, 0.3);
-  transition: height 0.2s ease;
-  opacity: 1 !important;
-  visibility: visible !important;
-  display: flex !important;
-  align-items: center;
-  z-index: 10;
-  pointer-events: auto !important; /* 确保进度条可点击 */
-}
-
-:deep(.video-js.vjs-user-active .vjs-progress-control),
-:deep(.video-js:hover .vjs-progress-control) {
-  height: 1.2em;
-}
-
-/* 音量控制样式 */
-:deep(.video-js .vjs-volume-panel) {
-  position: absolute;
-  right: 3.5em;
-}
-
-:deep(.video-js .vjs-progress-holder) {
-  height: 0.8em;
-  background: rgba(255, 255, 255, 0.2);
-  border-radius: 1em;
-  margin: 0 0.5em;
-  transition: height 0.2s ease;
-  display: flex !important;
-  align-items: center;
-  pointer-events: auto !important; /* 确保进度条可点击 */
-  cursor: pointer !important; /* 显示点击光标 */
-}
-
-:deep(.video-js.vjs-user-active .vjs-progress-holder),
-:deep(.video-js:hover .vjs-progress-holder) {
-  height: 1em;
-}
-
-/* 确保进度条上的所有元素都可以点击 */
-:deep(.video-js .vjs-progress-control .vjs-mouse-display),
-:deep(.video-js .vjs-progress-control .vjs-play-progress),
-:deep(.video-js .vjs-progress-control .vjs-load-progress),
-:deep(.video-js .vjs-progress-control .vjs-load-progress div) {
-  pointer-events: auto !important;
-}
-
-:deep(.video-js .vjs-play-progress) {
-  background: linear-gradient(to right, #3b82f6, #60a5fa);
-  border-radius: 1em;
-  box-shadow: 0 0 8px rgba(59, 130, 246, 0.5);
-}
-
-:deep(.video-js .vjs-play-progress:before) {
-  font-size: 0.9em;
-  top: -0.3em;
-}
-
-:deep(.video-js .vjs-load-progress) {
-  background: rgba(255, 255, 255, 0.3);
-  border-radius: 1em;
-}
-
-/* 时间显示样式 */
-:deep(.video-js .vjs-time-control) {
-  padding-left: 0.5em;
-  padding-right: 0.5em;
-  min-width: 2.2em;
-  font-family: "Arial", sans-serif;
-  font-weight: 500;
-  font-size: 0.9em;
-  opacity: 0.9;
-}
-
-:deep(.video-js .vjs-current-time) {
-  padding-right: 0.1em;
-}
-
-:deep(.video-js .vjs-duration) {
-  padding-left: 0.1em;
-}
-
-:deep(.video-js .vjs-time-divider) {
-  padding: 0;
-  min-width: 1em;
-  color: rgba(255, 255, 255, 0.8);
-}
-
-/* 快进快退按钮 */
-:deep(.video-js .vjs-replay-button) {
+/* 快进 10 秒按钮样式 */
+:deep(.video-js .vjs-forward-10-button) {
   font-size: 1.3em;
   position: relative;
 }
 
-:deep(.video-js .vjs-replay-button::before) {
-  content: "⟲";
+:deep(.video-js .vjs-forward-10-button::before) {
+  content: "\f11d";
+  font-family: VideoJS;
   text-shadow: 0 0 8px rgba(255, 255, 255, 0.3);
 }
 
-:deep(.video-js .vjs-forward-button) {
-  font-size: 1.3em;
-  position: relative;
+/* 确保进度条有足够的显示长度 */
+:deep(.video-js .vjs-progress-control) {
+  flex: 1 !important;
+  min-width: 0 !important;
 }
 
-:deep(.video-js .vjs-forward-button::before) {
-  content: "⟳";
-  text-shadow: 0 0 8px rgba(255, 255, 255, 0.3);
+:deep(.video-js .vjs-progress-holder) {
+  flex: 1 !important;
+  width: 100% !important;
 }
 
-/* 美化播放器整体外观 */
-:deep(.video-js) {
-  border-radius: 8px;
-  overflow: hidden;
-  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
+/* iOS 设备特殊样式 */
+@supports (-webkit-touch-callout: none) {
+  :deep(.video-js video) {
+    -webkit-playsinline: true;
+    playsinline: true;
+  }
+
+  :deep(.video-js) {
+    -webkit-transform: translateZ(0);
+    transform: translateZ(0);
+  }
 }
 
 .loading-overlay {
@@ -1155,32 +887,6 @@ defineExpose({
     margin-left: -0.75em;
   }
 
-  :deep(.video-js .vjs-control-bar) {
-    height: 3.5em;
-    padding: 0 0.8em;
-    opacity: 1;
-  }
-
-  :deep(.video-js .vjs-control) {
-    width: 2.8em;
-  }
-
-  :deep(.video-js .vjs-progress-control) {
-    bottom: 3.5em;
-    height: 0.8em;
-    opacity: 1 !important;
-    visibility: visible !important;
-  }
-
-  :deep(.video-js .vjs-progress-holder) {
-    height: 0.8em;
-  }
-
-  :deep(.video-js .vjs-time-control) {
-    min-width: 1.8em;
-    font-size: 0.8em;
-  }
-
   .error-content {
     padding: 18px;
     max-width: 260px;

+ 34 - 33
src/views/VideoPlayer.vue

@@ -49,12 +49,11 @@
     <!-- 视频播放器区域 -->
     <div class="relative rounded-2xl overflow-hidden bg-black">
       <div class="aspect-video video-container">
-        <!-- 只有付费用户才能看到播放器 -->
+        <!-- 统一使用VideoJSPlayer组件,根据用户权限决定是否传递视频源 -->
         <VideoJSPlayer
-          v-if="isSinglePurchased || isVip"
           ref="videoProcessorRef"
           :cover-url="videoInfo.cover"
-          :m3u8-url="videoInfo.m3u8"
+          :m3u8-url="isSinglePurchased || isVip ? videoInfo.m3u8 : ''"
           :alt="videoInfo.name"
           :auto-play="false"
           :hide-error="false"
@@ -70,15 +69,6 @@
           @canplay="onVideoCanPlay"
         />
 
-        <!-- 非付费用户只显示封面,使用VideoJSPlayer组件确保封面解密 -->
-        <VideoJSPlayer
-          v-else
-          :cover-url="videoInfo.cover"
-          :alt="videoInfo.name"
-          cover-class="w-full h-full object-cover"
-          @cover-loaded="onCoverLoaded"
-        />
-
         <!-- 非付费用户提示 -->
         <div
           v-if="!isSinglePurchased && !isVip"
@@ -817,8 +807,6 @@ const goBack = () => {
   };
   router.currentRoute.value.meta = returnMeta;
 
-  console.log("视频页返回,设置returnFromVideo标记", returnMeta);
-
   // 导航到主页
   router.push("/");
 };
@@ -1130,8 +1118,6 @@ const purchaseVideo = async () => {
 
       // 显示支付等待弹窗
       showSinglePaymentWaitingDialog.value = true;
-
-      console.log("单片购买订单创建成功:", response.out_trade_no);
     } else {
       console.error("单片购买失败:", response.msg);
       showError(`购买失败: ${response.msg || "未知错误"}`);
@@ -1159,21 +1145,23 @@ const checkVideoPurchaseStatus = async () => {
 
 // 加载视频信息
 const loadVideoInfo = async () => {
-  const vipLevel = userStore.getVipLevel();
-
-  // 获取视频ID(无论什么用户类型都使用相同的获取方式)
+  const userInfo = userStore.userInfo;
   const videoId = route.params.id || route.query.id;
 
   // 如果是guest或free用户,从query参数获取视频信息(只显示封面和名称)
-  if (vipLevel === VipLevel.GUEST || vipLevel === VipLevel.FREE) {
+  if (
+    !userInfo ||
+    userInfo.vipLevel === "guest" ||
+    userInfo.vipLevel === "free"
+  ) {
     const { cover, name, duration, view, like } = route.query;
 
-    if (cover) {
+    if (cover && name) {
       videoInfo.value = {
         id: videoId || "unknown",
-        name: name || "视频",
+        name: name as string,
         cover: cover as string,
-        m3u8: "", // 不提供 m3u8 地址
+        m3u8: "", // guest和free用户不提供 m3u8 地址
         duration: parseInt(duration as string) || 0,
         view: parseInt(view as string) || 0,
         like: parseInt(like as string) || 0,
@@ -1184,14 +1172,27 @@ const loadVideoInfo = async () => {
       // 设置页面标题
       document.title = `${videoInfo.value.name} - 视频`;
 
+      return;
+    } else {
+      // 设置默认值
+      videoInfo.value = {
+        id: videoId || "unknown",
+        name: "视频",
+        cover: "",
+        m3u8: "",
+        duration: 0,
+        view: 0,
+        like: 0,
+        time: 0,
+        taginfo: [],
+      };
       return;
     }
   }
 
-  // 其他情况,通过API获取视频详情
+  // 其他用户(会员或已购买用户),通过API获取视频详情
   if (videoId) {
     try {
-      // 通过API获取视频详情
       const response = await getVideoDetail(device, String(videoId));
 
       if (response.status === 0 && response.data) {
@@ -1200,7 +1201,7 @@ const loadVideoInfo = async () => {
           id: data.id || videoId,
           name: data.name || `视频 ${videoId}`,
           cover: data.cover || "",
-          m3u8: data.m3u8 || "",
+          m3u8: data.m3u8 || "", // 会员和已购买用户获取视频源
           duration: data.duration || 0,
           view: data.view || 0,
           like: data.like || 0,
@@ -1215,7 +1216,6 @@ const loadVideoInfo = async () => {
         // 设置页面标题
         document.title = `${videoInfo.value.name} - 视频播放`;
       } else {
-        console.error("获取视频详情失败-1:", response.msg);
         // 设置默认值
         videoInfo.value = {
           id: videoId,
@@ -1230,7 +1230,6 @@ const loadVideoInfo = async () => {
         };
       }
     } catch (error) {
-      console.error("获取视频详情失败-2:", error);
       // 设置默认值
       videoInfo.value = {
         id: videoId,
@@ -1244,8 +1243,6 @@ const loadVideoInfo = async () => {
         taginfo: [],
       };
     }
-  } else {
-    console.error("未找到视频ID");
   }
 };
 
@@ -1312,8 +1309,6 @@ watch(
     const queryChanged = JSON.stringify(newQuery) !== JSON.stringify(oldQuery);
 
     if (idChanged || queryChanged) {
-      // console.log("路由变化,重新加载视频", { idChanged, queryChanged });
-
       // 停止当前视频播放
       if (videoProcessorRef.value) {
         videoProcessorRef.value.stopVideo();
@@ -1327,10 +1322,11 @@ watch(
       await loadVideoInfo();
       await loadRelatedVideos();
 
-      // 检查购买状态(首页点击进入视频时的检查)
+      // 检查购买状态(只对非guest/free用户检查)
       const vipLevel = userStore.getVipLevel();
       if (
         vipLevel !== VipLevel.GUEST &&
+        vipLevel !== VipLevel.FREE &&
         videoInfo.value.id &&
         videoInfo.value.id !== "unknown"
       ) {
@@ -1342,6 +1338,11 @@ watch(
 );
 
 onMounted(async () => {
+  // 确保用户信息已同步
+  if (!userStore.userInfo) {
+    await userStore.sync();
+  }
+
   // 加载价格配置
   if (!priceStore.isPriceConfigLoaded) {
     await priceStore.fetchPriceConfig();