jinx 2 ماه پیش
والد
کامیت
90bc42bf50

+ 91 - 11
src/components/FileReader/epub.vue

@@ -30,7 +30,7 @@
         :disabled="!epubReady"
         @click="prevPage"
       >
-        <img :src="arrowIcon"  />
+        <img :src="arrowIcon" />
       </button>
 
       <div class="reader-stage">
@@ -44,7 +44,7 @@
         :disabled="!epubReady"
         @click="nextPage"
       >
-        <img :src="arrowIcon"  />
+        <img :src="arrowIcon" />
       </button>
     </section>
   </div>
@@ -53,10 +53,10 @@
 <script setup>
 import ePub from "epubjs";
 import {
+  computed,
   nextTick,
   onBeforeUnmount,
   onMounted,
-  computed,
   ref,
   watch,
 } from "vue";
@@ -75,6 +75,7 @@ const props = defineProps({
   },
 });
 
+const emit = defineEmits(["ready", "error"]);
 const epubContainer = ref(null);
 const epubError = ref("");
 const epubReady = ref(false);
@@ -83,6 +84,9 @@ const activeHref = ref("");
 
 let book = null;
 let rendition = null;
+let resizeObserver = null;
+let displayFallbackTimer = null;
+let displayRetryTimer = null;
 
 const sourceUrl = computed(() => {
   const rawUrl = props.url || props.filesList?.[0] || "";
@@ -160,6 +164,7 @@ const resizeReader = () => {
   if (!rendition || !epubContainer.value) {
     return;
   }
+
   setTimeout(() => {
     const { clientWidth, clientHeight } = epubContainer.value;
     if (!clientWidth || !clientHeight) {
@@ -170,9 +175,86 @@ const resizeReader = () => {
   }, 100);
 };
 
+const clearTimers = () => {
+  if (displayFallbackTimer) {
+    window.clearTimeout(displayFallbackTimer);
+    displayFallbackTimer = null;
+  }
+
+  if (displayRetryTimer) {
+    window.clearTimeout(displayRetryTimer);
+    displayRetryTimer = null;
+  }
+};
+
+const bindResizeObserver = () => {
+  if (!epubContainer.value || typeof ResizeObserver === "undefined") {
+    return;
+  }
+
+  resizeObserver?.disconnect();
+  resizeObserver = new ResizeObserver(() => {
+    resizeReader();
+  });
+  resizeObserver.observe(epubContainer.value);
+};
+
+const markReaderReady = () => {
+  if (epubReady.value) {
+    resizeReader();
+    return;
+  }
+
+  epubReady.value = true;
+  clearTimers();
+  bindResizeObserver();
+  resizeReader();
+  emit("ready");
+};
+
+const queueDisplay = (target) => {
+  if (!rendition) {
+    return;
+  }
+
+  clearTimers();
+
+  const displayResult = target
+    ? rendition.display(target)
+    : rendition.display();
+
+  Promise.resolve(displayResult)
+    .then(() => {
+      markReaderReady();
+    })
+    .catch(() => {
+      markReaderReady();
+    });
+
+  displayFallbackTimer = window.setTimeout(() => {
+    markReaderReady();
+  }, 900);
+
+  displayRetryTimer = window.setTimeout(() => {
+    if (!rendition || epubReady.value) {
+      return;
+    }
+
+    Promise.resolve(
+      target ? rendition.display(target) : rendition.display(),
+    ).catch(() => {});
+  }, 260);
+};
+
 const destroyEpub = () => {
+  clearTimers();
+  resizeObserver?.disconnect();
+  resizeObserver = null;
+
   if (rendition) {
     rendition.off("relocated", handleRelocated);
+    rendition.off("rendered", markReaderReady);
+    rendition.off("displayed", markReaderReady);
     rendition.destroy();
     rendition = null;
   }
@@ -214,14 +296,14 @@ const initEpub = async () => {
 
     applyTheme();
     rendition.on("relocated", handleRelocated);
+    rendition.on("rendered", markReaderReady);
+    rendition.on("displayed", markReaderReady);
 
-    await rendition.display();
     await book.ready;
-    resizeReader();
-    epubReady.value = true;
+    queueDisplay();
   } catch (error) {
-    alert(error);
-    epubError.value = error?.message || "EPUB加载失败";
+    epubError.value = error?.message || "EPUB 加载失败";
+    emit("error", error);
   }
 };
 
@@ -240,7 +322,7 @@ const nextPage = async () => {
 const jumpToChapter = async (href) => {
   if (rendition && href) {
     activeHref.value = normalizeHref(href);
-    await rendition.display(href);
+    queueDisplay(href);
   }
 };
 
@@ -274,7 +356,6 @@ onBeforeUnmount(() => {
   width: 254px;
   flex-shrink: 0;
   background: rgba(255, 255, 255, 0.92);
-  //   border-right: 1px dashed #c8b9a9;
   overflow: hidden;
 }
 
@@ -316,7 +397,6 @@ onBeforeUnmount(() => {
   line-height: 1.5;
   color: #9a9a9a;
   cursor: pointer;
-
   white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;

+ 15 - 1
src/components/FileReader/image.vue

@@ -2,7 +2,12 @@
   <div class="image-view">
     <div ref="viewerRef" class="image-viewer" @dblclick="resetView">
       <div ref="canvasRef" class="image-canvas">
-        <img class="image-content" :src="currentImage"/>
+        <img
+          class="image-content"
+          :src="currentImage"
+          @load="handleImageLoaded"
+          @error="handleImageError"
+        />
       </div>
     </div>
 
@@ -77,6 +82,7 @@ const props = defineProps({
   },
 });
 
+const emit = defineEmits(["ready", "error"]);
 const viewerRef = ref(null);
 const canvasRef = ref(null);
 const currentIndex = ref(props.initialIndex);
@@ -206,6 +212,14 @@ const toggleFullscreen = async () => {
   }
 };
 
+const handleImageLoaded = () => {
+  emit("ready");
+};
+
+const handleImageError = (event) => {
+  emit("error", event);
+};
+
 watch(
   () => currentImage.value,
   async () => {

+ 24 - 14
src/components/FileReader/pdf.vue

@@ -1,21 +1,20 @@
 <template>
   <div class="pdf-view" v-if="props.filesList.length">
     <div v-if="pdfError" class="pdf-empty">{{ pdfError }}</div>
-    <!-- image-resources-path="https://unpkg.com/pdfjs-dist@5.4.54/web/images/" -->
     <VuePdfEmbed
       v-else
-      annotation-layer
-      text-layer
       class="pdf-embed"
       :source="pdfSource"
-      @loading-failed="pdfError = $event?.message || 'PDF 加载失败'"
-      @rendering-failed="pdfError = $event?.message || 'PDF 渲染失败'"
+      @loaded="handleLoaded"
+      @loading-failed="handleLoadFailed"
+      @rendering-failed="handleRenderFailed"
     />
   </div>
+  
 </template>
 
 <script setup>
-import { computed, ref, defineProps } from "vue";
+import { computed, ref } from "vue";
 import VuePdfEmbed from "vue-pdf-embed";
 import { getThumb } from "@/utils/file";
 
@@ -29,23 +28,34 @@ const props = defineProps({
   },
   filesList: {
     type: Array,
-    default: [],
+    default: () => [],
   },
 });
-console.error(props.filesList);
+
+const emit = defineEmits(["ready", "error"]);
 const pdfError = ref("");
 
-const pdfSource = computed(() => ({
-  // cMapUrl: "https://unpkg.com/pdfjs-dist@5.4.54/cmaps/",
-  cMapUrl: getThumb(props.filesList[0]),
-  url: getThumb(props.filesList[0]),
-}));
+const pdfSource = computed(() => getThumb(props.filesList[0]) || "");
+
+const handleLoaded = () => {
+  pdfError.value = "";
+  emit("ready");
+};
+
+const handleLoadFailed = (error) => {
+  pdfError.value = error?.message || "PDF 加载失败";
+  emit("error", error);
+};
+
+const handleRenderFailed = (error) => {
+  pdfError.value = error?.message || "PDF 渲染失败";
+  emit("error", error);
+};
 </script>
 
 <style lang="scss" scoped>
 .pdf-view {
   width: 567px;
-  //   min-height: calc(100vh - 120px);
   height: 80vh;
   margin: 0 auto;
   overflow: auto;

+ 12 - 0
src/components/FileReader/video.vue

@@ -6,6 +6,8 @@
       preload="metadata"
       class="video-player"
       :src="sourceUrl"
+      @loadeddata="handleVideoLoaded"
+      @error="handleVideoError"
     ></video>
     <div v-else class="video-empty">暂无可播放视频</div>
   </div>
@@ -26,10 +28,20 @@ const props = defineProps({
   },
 });
 
+const emit = defineEmits(["ready", "error"]);
+
 const sourceUrl = computed(() => {
   const rawUrl = props.url || props.filesList?.[0] || "";
   return rawUrl ? getThumb(rawUrl) : "";
 });
+
+const handleVideoLoaded = () => {
+  emit("ready");
+};
+
+const handleVideoError = (event) => {
+  emit("error", event);
+};
 </script>
 
 <style lang="scss" scoped>

+ 4 - 4
src/main.js

@@ -12,7 +12,7 @@ import store from "./store";
 import "./style.css";
 let router = browser.isMobile() ? routerM : routerP;
 let App = browser.isMobile() ? Mobile : Pc;
-// setCurrentRouter(router);
-// createApp(App).use(store).use(router).use(ElementPlus).mount("#app");
-setCurrentRouter(routerP);
-createApp(Pc).use(store).use(routerP).use(ElementPlus).mount("#app");
+setCurrentRouter(router);
+createApp(App).use(store).use(router).use(ElementPlus).mount("#app");
+// setCurrentRouter(routerP);
+// createApp(Pc).use(store).use(routerP).use(ElementPlus).mount("#app");

+ 21 - 26
src/router/index.js

@@ -206,36 +206,31 @@ router.beforeEach(async (to) => {
       return true;
     }
 
-    // if (!hasUserInfo && token) {
-    //   try {
-    //     const res = await userDetail();
-    //     if (res?.data) {
-    //       store.commit("setStore", { userInfo: res.data });
-    //       return { name: "home" };
-    //     }
-    //   } catch {
-    //     localStorage.removeItem("token");
-    //     return true;
-    //   }
-    // }
+    if (!hasUserInfo && token) {
+      try {
+        const res = await userDetail();
+        if (res?.data) {
+          store.commit("setStore", { userInfo: res.data });
+          return { name: "home" };
+        }
+      } catch {
+        localStorage.removeItem("token");
+        return true;
+      }
+    }
 
     return { name: "home" };
-  } else {
-    const res = await userDetail();
-    if (res?.data) {
-      store.commit("setStore", { userInfo: res.data });
+  }
+  if (token && !hasUserInfo) {
+    try {
+      const res = await userDetail();
+      if (res?.data) {
+        store.commit("setStore", { userInfo: res.data });
+      }
+    } catch {
+      localStorage.removeItem("token");
     }
   }
-  // if (token && !hasUserInfo) {
-  //   try {
-  //     const res = await userDetail();
-  //     if (res?.data) {
-  //       store.commit("setStore", { userInfo: res.data });
-  //     }
-  //   } catch {
-  //     localStorage.removeItem("token");
-  //   }
-  // }
 
   return true;
 });

+ 1 - 6
src/views/mobile/components/FileReader/pdf.vue

@@ -3,8 +3,6 @@
     <div v-if="pdfError" class="pdf-empty">{{ pdfError }}</div>
     <VuePdfEmbed
       v-else
-      annotation-layer
-      text-layer
       class="pdf-embed"
       :source="pdfSource"
       @loading-failed="pdfError = $event?.message || 'PDF 加载失败'"
@@ -33,10 +31,7 @@ const props = defineProps({
 });
 const pdfError = ref("");
 
-const pdfSource = computed(() => ({
-  cMapUrl: getThumb(props.filesList[0]),
-  url: getThumb(props.filesList[0]),
-}));
+const pdfSource = computed(() => getThumb(props.filesList[0]) || "");
 </script>
 
 <style lang="scss" scoped>

+ 42 - 6
src/views/pc/ReadingDetailView.vue

@@ -3,15 +3,25 @@
     <div class="reading-stage">
       <component
         :is="currentPage.page"
-        v-if="!isLoading && currentPage && filesList.length"
+        v-if="currentPage && filesList.length"
         :key="`${currentPage.type}-${filesList.join('|')}`"
         v-bind="readerProps"
+        @ready="handleReaderReady"
+        @error="handleReaderError"
       ></component>
-      <div v-else-if="isLoading" class="reading-loading">
+
+      <div
+        v-if="isLoading"
+        class="reading-loading"
+        :class="{ 'reading-loading-overlay': filesList.length > 0 }"
+      >
         <div class="loading-spinner"></div>
         <p>资源加载中...</p>
       </div>
-      <div v-else class="reading-empty">暂无可预览文件</div>
+
+      <div v-else-if="!filesList.length" class="reading-empty">
+        暂无可预览文件
+      </div>
     </div>
   </div>
 </template>
@@ -59,12 +69,15 @@ const currentPage = computed(
   () => fileTypes.find((item) => item.type === currentType.value) || fileTypes[0],
 );
 const filesList = ref([]);
-const isLoading = ref(false);
+const detailLoading = ref(false);
+const readerLoading = ref(false);
 const pendingReadSeconds = ref(0);
 
 let readTimer = null;
 let isPageLeaving = false;
 
+const isLoading = computed(() => detailLoading.value || readerLoading.value);
+
 const getShouldTrackReadTime = () => {
   if (typeof document === "undefined") {
     return false;
@@ -185,8 +198,17 @@ const handlePageHide = () => {
   submitReadTime(true);
 };
 
+const handleReaderReady = () => {
+  readerLoading.value = false;
+};
+
+const handleReaderError = () => {
+  readerLoading.value = false;
+};
+
 const getDetails = () => {
-  isLoading.value = true;
+  detailLoading.value = true;
+  readerLoading.value = false;
 
   formDataDetail(formDefId, formDataId)
     .then((res) => {
@@ -203,9 +225,15 @@ const getDetails = () => {
       filesList.value = sourceFiles.filter((item) =>
         currentPage.value.format.includes(getFileExt(item)),
       );
+
+      readerLoading.value = filesList.value.length > 0;
+    })
+    .catch(() => {
+      filesList.value = [];
+      readerLoading.value = false;
     })
     .finally(() => {
-      isLoading.value = false;
+      detailLoading.value = false;
     });
 };
 
@@ -223,6 +251,7 @@ watch(
   () => route.fullPath,
   async () => {
     filesList.value = [];
+    readerLoading.value = false;
     await nextTick();
     getDetails();
   },
@@ -283,6 +312,13 @@ onBeforeUnmount(() => {
   }
 }
 
+.reading-loading-overlay {
+  position: absolute;
+  inset: 0;
+  z-index: 5;
+  background: rgba(255, 255, 255, 0.72);
+}
+
 .loading-spinner {
   width: 42px;
   height: 42px;