|
|
@@ -88,11 +88,14 @@
|
|
|
ref="chapter"
|
|
|
>
|
|
|
<chapter-content
|
|
|
+ ref="chapterRef"
|
|
|
+ :chapterIndex="data.index"
|
|
|
:contents="data.content"
|
|
|
:title="data.title"
|
|
|
:spacing="store.config.spacing"
|
|
|
:fontSize="fontSize"
|
|
|
:fontFamily="fontFamily"
|
|
|
+ @readedLengthChange="onReadedLengthChange"
|
|
|
v-if="showContent"
|
|
|
/>
|
|
|
</div>
|
|
|
@@ -109,10 +112,10 @@ import settings from "@/plugins/config";
|
|
|
import API from "@api";
|
|
|
import loadingSvg from "@element-plus/icons-svg/loading.svg?raw";
|
|
|
|
|
|
+// loading spinner
|
|
|
const showLoading = ref(false);
|
|
|
const loadingSerive = ref(null);
|
|
|
const content = ref();
|
|
|
-
|
|
|
watch(showLoading, (loading) => {
|
|
|
if (!loading) return loadingSerive.value?.close();
|
|
|
loadingSerive.value = ElLoading.service({
|
|
|
@@ -132,13 +135,6 @@ try {
|
|
|
localStorage.removeItem("config");
|
|
|
}
|
|
|
|
|
|
-const loading = ref();
|
|
|
-const noPoint = ref(true);
|
|
|
-const showToolBar = ref(false);
|
|
|
-const chapterData = ref([]);
|
|
|
-const scrollObserve = ref(null);
|
|
|
-const readingObserve = ref(null);
|
|
|
-
|
|
|
const {
|
|
|
catalog,
|
|
|
popCataVisible,
|
|
|
@@ -147,6 +143,7 @@ const {
|
|
|
showContent,
|
|
|
config,
|
|
|
readingBook,
|
|
|
+ bookProgress,
|
|
|
} = storeToRefs(store);
|
|
|
const chapterPos = computed({
|
|
|
get: () => readingBook.value.chapterPos,
|
|
|
@@ -201,6 +198,7 @@ const chapterTheme = computed(() => {
|
|
|
width: readWidth.value,
|
|
|
};
|
|
|
});
|
|
|
+const showToolBar = ref(false);
|
|
|
const leftBarTheme = computed(() => {
|
|
|
return {
|
|
|
background: popupColor.value,
|
|
|
@@ -221,28 +219,25 @@ const rightBarTheme = computed(() => {
|
|
|
});
|
|
|
const isNight = computed(() => theme.value == 6);
|
|
|
|
|
|
-watchEffect(() => {
|
|
|
- if (chapterData.value.length > 0) {
|
|
|
- store.setContentLoading(false);
|
|
|
- //添加章节内容到observe
|
|
|
- addReadingObserve();
|
|
|
- }
|
|
|
-});
|
|
|
-
|
|
|
-watchEffect(() => {
|
|
|
- document.title = catalog.value[chapterIndex.value]?.title || document.title;
|
|
|
- store.saveBookProcess();
|
|
|
-});
|
|
|
+// 顶部底部跳转
|
|
|
+const top = ref();
|
|
|
+const bottom = ref();
|
|
|
+const toTop = () => {
|
|
|
+ jump(top.value);
|
|
|
+};
|
|
|
+const toBottom = () => {
|
|
|
+ jump(bottom.value);
|
|
|
+};
|
|
|
|
|
|
-watchEffect(() => {
|
|
|
- if (!infiniteLoading.value) {
|
|
|
- scrollObserve.value?.disconnect();
|
|
|
- } else {
|
|
|
- scrollObserve.value?.observe(loading.value);
|
|
|
- }
|
|
|
-});
|
|
|
+// 书架路由切换
|
|
|
+const router = useRouter();
|
|
|
+const toShelf = () => {
|
|
|
+ router.push("/");
|
|
|
+};
|
|
|
|
|
|
-const top = ref();
|
|
|
+// 获取章节内容
|
|
|
+const chapterData = ref([]);
|
|
|
+const noPoint = ref(true);
|
|
|
const getContent = (index, reloadChapter = true, chapterPos = 0) => {
|
|
|
if (reloadChapter) {
|
|
|
//展示进度条
|
|
|
@@ -252,6 +247,7 @@ const getContent = (index, reloadChapter = true, chapterPos = 0) => {
|
|
|
jump(top.value, { duration: 0 });
|
|
|
//从目录,按钮切换章节时保存进度 预加载时不保存
|
|
|
saveReadingBookProgressToBrowser(index, chapterPos);
|
|
|
+ chapterData.value = [];
|
|
|
}
|
|
|
let bookUrl = sessionStorage.getItem("bookUrl");
|
|
|
let { title, index: chapterIndex } = catalog.value[index];
|
|
|
@@ -261,12 +257,12 @@ const getContent = (index, reloadChapter = true, chapterPos = 0) => {
|
|
|
if (res.data.isSuccess) {
|
|
|
let data = res.data.data;
|
|
|
let content = data.split(/\n+/);
|
|
|
- updateChapterData({ index, content, title }, reloadChapter);
|
|
|
+ chapterData.value.push({ index, content, title });
|
|
|
if (reloadChapter) toChapterPos(chapterPos);
|
|
|
} else {
|
|
|
ElMessage({ message: res.data.errorMsg, type: "error" });
|
|
|
let content = [res.data.errorMsg];
|
|
|
- updateChapterData({ index, content, title }, reloadChapter);
|
|
|
+ chapterData.value.push({ index, content, title });
|
|
|
}
|
|
|
store.setContentLoading(true);
|
|
|
showLoading.value = false;
|
|
|
@@ -279,55 +275,69 @@ const getContent = (index, reloadChapter = true, chapterPos = 0) => {
|
|
|
(err) => {
|
|
|
ElMessage({ message: "获取章节内容失败", type: "error" });
|
|
|
let content = ["获取章节内容失败!"];
|
|
|
- updateChapterData({ index, content, title }, reloadChapter);
|
|
|
+ chapterData.value.push({ index, content, title });
|
|
|
showLoading.value = false;
|
|
|
store.setShowContent(true);
|
|
|
throw err;
|
|
|
}
|
|
|
);
|
|
|
};
|
|
|
+
|
|
|
+// 章节进度跳转和计算
|
|
|
const chapter = ref();
|
|
|
+const chapterRef = ref();
|
|
|
const toChapterPos = (pos) => {
|
|
|
nextTick(() => {
|
|
|
- let wordCount = 0;
|
|
|
- if (chapter.value.length != 1) return;
|
|
|
- for (let element of chapter.value[0].children) {
|
|
|
- wordCount += parseInt(element.getAttribute("wordCount")) + 1; //计算换行符
|
|
|
- if (wordCount - 1 >= pos) {
|
|
|
- //最后一段没有换行符
|
|
|
- jump(element, {
|
|
|
- duration: 0,
|
|
|
- });
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
+ if (chapterRef.value.length === 1)
|
|
|
+ chapterRef.value[0].scrollToReadedLength(pos);
|
|
|
});
|
|
|
};
|
|
|
-//计算当前章节阅读的字数
|
|
|
-const computeChapterPos = () => {
|
|
|
- if (chapter.value.length == 0) return;
|
|
|
- //计算当前阅读进度对应的element
|
|
|
- let chapterElement = chapter.value.find(
|
|
|
- (element) => element.getAttribute("chapterIndex") == chapterIndex.value
|
|
|
- );
|
|
|
- if (!chapterElement) return;
|
|
|
- //计算已读字数
|
|
|
- let mChapterPos = 0;
|
|
|
- for (let paragraph of chapterElement.children) {
|
|
|
- mChapterPos += parseInt(paragraph.getAttribute("wordCount")) + 1; //计算换行符
|
|
|
- if (paragraph.getBoundingClientRect().top >= 0) {
|
|
|
- chapterPos.value = mChapterPos - 1; //最后一段没有换行符
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
+const onReadedLengthChange = (index, pos) => {
|
|
|
+ saveReadingBookProgressToBrowser(index, pos);
|
|
|
};
|
|
|
-const bottom = ref();
|
|
|
-const toTop = () => {
|
|
|
- jump(top.value);
|
|
|
+
|
|
|
+// 文档标题
|
|
|
+watchEffect(() => {
|
|
|
+ document.title = catalog.value[chapterIndex.value]?.title || document.title;
|
|
|
+});
|
|
|
+
|
|
|
+// 阅读记录保存浏览器
|
|
|
+const saveReadingBookProgressToBrowser = (index, pos) => {
|
|
|
+ //保存localStorage
|
|
|
+ let bookUrl = sessionStorage.getItem("bookUrl");
|
|
|
+ var book = JSON.parse(localStorage.getItem(bookUrl));
|
|
|
+ book.index = index;
|
|
|
+ book.chapterPos = pos;
|
|
|
+ localStorage.setItem(bookUrl, JSON.stringify(book));
|
|
|
+ //最近阅读
|
|
|
+ book = JSON.parse(localStorage.getItem("readingRecent"));
|
|
|
+ book.chapterIndex = index;
|
|
|
+ book.chapterPos = pos;
|
|
|
+ localStorage.setItem("readingRecent", JSON.stringify(book));
|
|
|
+ //保存vuex
|
|
|
+ chapterIndex.value = index;
|
|
|
+ chapterPos.value = pos;
|
|
|
+ //保存sessionStorage
|
|
|
+ sessionStorage.setItem("chapterIndex", index);
|
|
|
+ sessionStorage.setItem("chapterPos", String(pos));
|
|
|
};
|
|
|
-const toBottom = () => {
|
|
|
- jump(bottom.value);
|
|
|
+
|
|
|
+// 进度同步
|
|
|
+// 返回同步 同步请求会在获取书架前完成
|
|
|
+// 刷新 关闭页面
|
|
|
+const syncBookProgress = () => {
|
|
|
+ console.log("page hide");
|
|
|
+ if (!bookProgress.value) return;
|
|
|
+ // 常规请求可能会被取消 使用Fetch keep-alive 或者 navigator.sendBeacon
|
|
|
+ navigator.sendBeacon(
|
|
|
+ `${import.meta.env.VITE_API || location.origin}/saveBookProgress`,
|
|
|
+ JSON.stringify(bookProgress.value)
|
|
|
+ );
|
|
|
};
|
|
|
+
|
|
|
+// 定时同步
|
|
|
+
|
|
|
+// 章节切换
|
|
|
const toNextChapter = () => {
|
|
|
store.setContentLoading(true);
|
|
|
let index = chapterIndex.value + 1;
|
|
|
@@ -360,42 +370,33 @@ const toPreChapter = () => {
|
|
|
});
|
|
|
}
|
|
|
};
|
|
|
-const saveReadingBookProgressToBrowser = (index, pos = chapterPos.value) => {
|
|
|
- //保存localStorage
|
|
|
- let bookUrl = sessionStorage.getItem("bookUrl");
|
|
|
- var book = JSON.parse(localStorage.getItem(bookUrl));
|
|
|
- book.index = index;
|
|
|
- book.chapterPos = pos;
|
|
|
- localStorage.setItem(bookUrl, JSON.stringify(book));
|
|
|
- //最近阅读
|
|
|
- book = JSON.parse(localStorage.getItem("readingRecent"));
|
|
|
- book.chapterIndex = index;
|
|
|
- book.chapterPos = pos;
|
|
|
- localStorage.setItem("readingRecent", JSON.stringify(book));
|
|
|
- //保存vuex
|
|
|
- chapterIndex.value = index;
|
|
|
- chapterPos.value = pos;
|
|
|
- //保存sessionStorage
|
|
|
- sessionStorage.setItem("chapterIndex", index);
|
|
|
- sessionStorage.setItem("chapterPos", String(pos));
|
|
|
-};
|
|
|
-const updateChapterData = async (data, reloadChapter) => {
|
|
|
- if (reloadChapter) {
|
|
|
- chapterData.value.splice(0);
|
|
|
+
|
|
|
+// 无限滚动
|
|
|
+const scrollObserve = ref(null);
|
|
|
+const loading = ref();
|
|
|
+watchEffect(() => {
|
|
|
+ if (!infiniteLoading.value) {
|
|
|
+ scrollObserve.value?.disconnect();
|
|
|
+ } else {
|
|
|
+ scrollObserve.value?.observe(loading.value);
|
|
|
}
|
|
|
- chapterData.value.push(data);
|
|
|
-};
|
|
|
+});
|
|
|
const loadMore = () => {
|
|
|
let index = chapterData.value.slice(-1)[0].index;
|
|
|
if (catalog.value.length - 1 > index) {
|
|
|
getContent(index + 1, false);
|
|
|
}
|
|
|
};
|
|
|
-const router = useRouter();
|
|
|
-const toShelf = () => {
|
|
|
- router.push("/");
|
|
|
+// IntersectionObserver回调 底部加载
|
|
|
+const handleIScrollObserve = (entries) => {
|
|
|
+ if (showLoading.value) return;
|
|
|
+ for (let { isIntersecting } of entries) {
|
|
|
+ if (!isIntersecting) return;
|
|
|
+ loadMore();
|
|
|
+ }
|
|
|
};
|
|
|
-//监听方向键
|
|
|
+
|
|
|
+// 监听方向键
|
|
|
const handleKeyPress = (event) => {
|
|
|
switch (event.key) {
|
|
|
case "ArrowLeft":
|
|
|
@@ -438,52 +439,6 @@ const handleKeyPress = (event) => {
|
|
|
break;
|
|
|
}
|
|
|
};
|
|
|
-//IntersectionObserver回调 底部加载
|
|
|
-const handleIScrollObserve = (entries) => {
|
|
|
- if (showLoading.value) return;
|
|
|
- for (let { isIntersecting } of entries) {
|
|
|
- if (!isIntersecting) return;
|
|
|
- loadMore();
|
|
|
- }
|
|
|
-};
|
|
|
-//IntersectionObserver回调 当前阅读章节序号
|
|
|
-const handleIReadingObserve = (entries) => {
|
|
|
- nextTick(() => {
|
|
|
- for (let { isIntersecting, target, boundingClientRect } of entries) {
|
|
|
- let chapterTitleIndex = parseInt(target.getAttribute("chapterIndex"));
|
|
|
- if (isIntersecting) {
|
|
|
- chapterIndex.value = chapterTitleIndex;
|
|
|
- } else {
|
|
|
- if (boundingClientRect.top < 0) {
|
|
|
- chapterIndex.value = chapterTitleIndex + 1;
|
|
|
- } else {
|
|
|
- chapterIndex.value = chapterTitleIndex - 1;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
-};
|
|
|
-//添加所有章节到observe
|
|
|
-const addReadingObserve = () => {
|
|
|
- nextTick(() => {
|
|
|
- let chapterElements = chapter.value;
|
|
|
- if (!chapterElements) return;
|
|
|
- chapterElements.forEach((el) => readingObserve.value.observe(el));
|
|
|
- });
|
|
|
-};
|
|
|
-
|
|
|
-onBeforeRouteLeave((to, from, next) => {
|
|
|
- computeChapterPos();
|
|
|
- saveReadingBookProgressToBrowser(chapterIndex.value);
|
|
|
- next();
|
|
|
-});
|
|
|
-/*
|
|
|
-window.addEventListener("beforeunload", (e) => {
|
|
|
- e.preventDefault();
|
|
|
- e.returnValue = "";
|
|
|
- alert(111);
|
|
|
-});
|
|
|
-*/
|
|
|
|
|
|
onMounted(() => {
|
|
|
showLoading.value = true;
|
|
|
@@ -523,13 +478,12 @@ onMounted(() => {
|
|
|
|
|
|
getContent(chapterIndex, true, chapterPos);
|
|
|
window.addEventListener("keyup", handleKeyPress);
|
|
|
+ window.addEventListener("visibilitychange", syncBookProgress);
|
|
|
//监听底部加载
|
|
|
scrollObserve.value = new IntersectionObserver(handleIScrollObserve, {
|
|
|
rootMargin: "-100% 0% 20% 0%",
|
|
|
});
|
|
|
infiniteLoading.value && scrollObserve.value.observe(loading.value);
|
|
|
- //监听当前阅读章节
|
|
|
- readingObserve.value = new IntersectionObserver(handleIReadingObserve);
|
|
|
//第二次点击同一本书 页面标题不会变化
|
|
|
document.title = null;
|
|
|
document.title = bookName + " | " + catalog.value[chapterIndex].title;
|
|
|
@@ -544,10 +498,10 @@ onMounted(() => {
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
window.removeEventListener("keyup", handleKeyPress);
|
|
|
+ window.removeEventListener("visibilitychange", syncBookProgress);
|
|
|
readSettingsVisible.value = false;
|
|
|
popCataVisible.value = false;
|
|
|
scrollObserve.value?.disconnect();
|
|
|
- readingObserve.value?.disconnect();
|
|
|
});
|
|
|
</script>
|
|
|
|