|
|
@@ -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;
|