refactor(modules/web): Mirgrat to typescript; fix bugs

This commit is contained in:
Xwite
2024-10-14 21:46:45 +08:00
parent b95deeb607
commit 7984c45cb9
81 changed files with 6396 additions and 3166 deletions
+299 -274
View File
@@ -106,25 +106,18 @@
</div>
</template>
<script setup>
import jump from "@/plugins/jump";
import settings from "@/config/themeConfig";
import API from "@api";
import { useLoading } from "@/hooks/loading";
import { useThrottleFn } from "@vueuse/shared";
<script setup lang="ts">
import jump from '@/plugins/jump'
import settings from '@/config/themeConfig'
import API from '@api'
import { useLoading } from '@/hooks/loading'
import { useThrottleFn } from '@vueuse/shared'
import { isNullOrBlank } from '@/utils/utils'
const content = ref();
const content = ref()
// loading spinner
const { isLoading, loadingWrapper } = useLoading(content, "正在获取信息");
const store = useBookStore();
// 读取阅读配置
try {
const browerConfig = JSON.parse(localStorage.getItem("config"));
if (browerConfig != null) store.setConfig(browerConfig);
} catch {
localStorage.removeItem("config");
}
const { isLoading, loadingWrapper } = useLoading(content, '正在获取信息')
const store = useBookStore()
const {
catalog,
@@ -135,232 +128,238 @@ const {
bookProgress,
theme,
isNight,
} = storeToRefs(store);
} = storeToRefs(store)
const chapterPos = computed({
get: () => store.readingBook.chapterPos,
set: (value) => (store.readingBook.chapterPos = value),
});
set: value => (store.readingBook.chapterPos = value),
})
const chapterIndex = computed({
get: () => store.readingBook.index,
set: (value) => (store.readingBook.index = value),
});
get: () => store.readingBook.chapterIndex,
set: value => (store.readingBook.chapterIndex = value),
})
const isSeachBook = computed({
get: () => store.readingBook.isSeachBook,
set: value => (store.readingBook.isSeachBook = value),
})
// 当前阅读书籍readingBook持久化
watch(
() => store.readingBook,
book => {
// 保存localStorage
// localStorage.setItem(book.bookUrl, JSON.stringify(book));
// 最近阅读
localStorage.setItem('readingRecent', JSON.stringify(book))
//保存 sessionStorage
sessionStorage.setItem('chapterIndex', book.chapterIndex.toString())
sessionStorage.setItem('chapterPos', book.chapterPos.toString())
},
)
// 无限滚动
const infiniteLoading = computed(() => store.config.infiniteLoading);
let scrollObserver;
const loading = ref();
const infiniteLoading = computed(() => store.config.infiniteLoading)
let scrollObserver: IntersectionObserver | null
const loading = ref()
watchEffect(() => {
if (!infiniteLoading.value) {
scrollObserver?.disconnect();
scrollObserver?.disconnect()
} else {
scrollObserver?.observe(loading.value);
scrollObserver?.observe(loading.value)
}
});
})
const loadMore = () => {
let index = chapterData.value.slice(-1)[0].index;
const index = chapterData.value.slice(-1)[0].index
if (catalog.value.length - 1 > index) {
getContent(index + 1, false);
store.saveBookProgress(); // 保存的是上一章的进度,不是预载的本章进度
getContent(index + 1, false)
store.saveBookProgress() // 保存的是上一章的进度,不是预载的本章进度
}
};
}
// IntersectionObserver回调 底部加载
const onReachBottom = (entries) => {
if (isLoading.value) return;
for (let { isIntersecting } of entries) {
if (!isIntersecting) return;
loadMore();
const onReachBottom = (entries: IntersectionObserverEntry[]) => {
if (isLoading.value) return
for (const { isIntersecting } of entries) {
if (!isIntersecting) return
loadMore()
}
};
}
// 字体
const fontFamily = computed(() => {
if (store.config.font >= 0) {
return settings.fonts[store.config.font];
return settings.fonts[store.config.font]
}
return store.config.customFontName;
});
return store.config.customFontName
})
const fontSize = computed(() => {
return store.config.fontSize + "px";
});
return store.config.fontSize + 'px'
})
// 主题部分
const bodyColor = computed(() => settings.themes[theme.value].body);
const chapterColor = computed(() => settings.themes[theme.value].content);
const popupColor = computed(() => settings.themes[theme.value].popup);
const bodyColor = computed(() => settings.themes[theme.value].body)
const chapterColor = computed(() => settings.themes[theme.value].content)
const popupColor = computed(() => settings.themes[theme.value].popup)
const readWidth = computed(() => {
if (!miniInterface.value) {
return store.config.readWidth - 130 + "px";
return store.config.readWidth - 130 + 'px'
} else {
return window.innerWidth + "px";
return window.innerWidth + 'px'
}
});
})
const popupWidth = computed(() => {
if (!miniInterface.value) {
return store.config.readWidth - 33;
return store.config.readWidth - 33
} else {
return window.innerWidth - 33;
return window.innerWidth - 33
}
});
})
const bodyTheme = computed(() => {
return {
background: bodyColor.value,
};
});
}
})
const chapterTheme = computed(() => {
return {
background: chapterColor.value,
width: readWidth.value,
};
});
const showToolBar = ref(false);
}
})
const showToolBar = ref(false)
const leftBarTheme = computed(() => {
return {
background: popupColor.value,
marginLeft: miniInterface.value
? 0
: -(store.config.readWidth / 2 + 68) + "px",
display: miniInterface.value && !showToolBar.value ? "none" : "block",
};
});
: -(store.config.readWidth / 2 + 68) + 'px',
display: miniInterface.value && !showToolBar.value ? 'none' : 'block',
}
})
const rightBarTheme = computed(() => {
return {
background: popupColor.value,
marginRight: miniInterface.value
? 0
: -(store.config.readWidth / 2 + 52) + "px",
display: miniInterface.value && !showToolBar.value ? "none" : "block",
};
});
: -(store.config.readWidth / 2 + 52) + 'px',
display: miniInterface.value && !showToolBar.value ? 'none' : 'block',
}
})
/**
* pc移动端判断 最大阅读宽度修正
* 阅读宽度最小为640px 加上工具栏 68px 52px 取较大值 为 776px
*/
const onResize = () => {
store.setMiniInterface(window.innerWidth < 776);
const width = store.config.readWidth; /**包含padding */
checkPageWidth(width);
};
store.setMiniInterface(window.innerWidth < 776)
const width = store.config.readWidth /**包含padding */
checkPageWidth(width)
}
/** 判断阅读宽度是否超出页面 */
const checkPageWidth = (readWidth) => {
if (store.miniInterface) return;
if (readWidth + 2 * 68 > window.innerWidth) store.config.readWidth -= 160;
};
const checkPageWidth = (readWidth: number) => {
if (store.miniInterface) return
if (readWidth + 2 * 68 > window.innerWidth) store.config.readWidth -= 160
}
watch(
() => store.config.readWidth,
(width) => checkPageWidth(width),
);
width => checkPageWidth(width),
)
// 顶部底部跳转
const top = ref();
const bottom = ref();
const top = ref()
const bottom = ref()
const toTop = () => {
jump(top.value);
};
jump(top.value)
}
const toBottom = () => {
jump(bottom.value);
};
jump(bottom.value)
}
// 书架路由切换
const router = useRouter();
const router = useRouter()
const toShelf = () => {
router.push("/");
};
router.push('/')
}
// 获取章节内容
const chapterData = ref([]);
const noPoint = ref(true);
const getContent = (index, reloadChapter = true, chapterPos = 0) => {
const chapterData = ref<{ index: number; content: string[]; title: string }[]>(
[],
)
const noPoint = ref(true)
const getContent = (index: number, reloadChapter = true, chapterPos = 0) => {
if (reloadChapter) {
//展示进度条
store.setShowContent(false);
store.setShowContent(false)
//强制滚回顶层
jump(top.value, { duration: 0 });
jump(top.value, { duration: 0 })
//从目录,按钮切换章节时保存进度 预加载时不保存
saveReadingBookProgressToBrowser(index, chapterPos);
chapterData.value = [];
saveReadingBookProgressToBrowser(index, chapterPos)
chapterData.value = []
}
let bookUrl = sessionStorage.getItem("bookUrl");
let { title, index: chapterIndex } = catalog.value[index];
const bookUrl = store.readingBook.bookUrl
const { title, index: chapterIndex } = catalog.value[index]
loadingWrapper(
API.getBookContent(bookUrl, chapterIndex).then(
(res) => {
res => {
if (res.data.isSuccess) {
let data = res.data.data;
let content = data.split(/\n+/);
chapterData.value.push({ index, content, title });
if (reloadChapter) toChapterPos(chapterPos);
const data = res.data.data
const content = data.split(/\n+/)
chapterData.value.push({ index, content, title })
if (reloadChapter) toChapterPos(chapterPos)
} else {
ElMessage({ message: res.data.errorMsg, type: "error" });
let content = [res.data.errorMsg];
chapterData.value.push({ index, content, title });
ElMessage({ message: res.data.errorMsg, type: 'error' })
const content = [res.data.errorMsg]
chapterData.value.push({ index, content, title })
}
store.setContentLoading(true);
noPoint.value = false;
store.setShowContent(true);
store.setContentLoading(true)
noPoint.value = false
store.setShowContent(true)
if (!res.data.isSuccess) {
throw res.data;
throw res.data
}
},
(err) => {
ElMessage({ message: "获取章节内容失败", type: "error" });
let content = ["获取章节内容失败!"];
chapterData.value.push({ index, content, title });
store.setShowContent(true);
throw err;
err => {
ElMessage({ message: '获取章节内容失败', type: 'error' })
const content = ['获取章节内容失败!']
chapterData.value.push({ index, content, title })
store.setShowContent(true)
throw err
},
),
);
};
)
}
// 章节进度跳转和计算
const chapter = ref();
const chapterRef = ref();
const toChapterPos = (pos) => {
const chapter = ref()
const chapterRef = ref()
const toChapterPos = (pos: number) => {
nextTick(() => {
if (chapterRef.value.length === 1)
chapterRef.value[0].scrollToReadedLength(pos);
});
};
chapterRef.value[0].scrollToReadedLength(pos)
})
}
// 60秒保存一次进度
const saveBookProgressThrottle = useThrottleFn(
() => store.saveBookProgress(),
60000,
);
)
const onReadedLengthChange = (index, pos) => {
saveReadingBookProgressToBrowser(index, pos);
saveBookProgressThrottle();
};
const onReadedLengthChange = (index: number, pos: number) => {
saveReadingBookProgressToBrowser(index, pos)
saveBookProgressThrottle()
}
// 文档标题
watchEffect(() => {
document.title = catalog.value[chapterIndex.value]?.title || document.title;
});
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 saveReadingBookProgressToBrowser = (index: number, pos: number) => {
// 保存pinia
chapterIndex.value = index
chapterPos.value = pos
}
// 进度同步
// 返回导航变化 同步请求会在获取书架前完成
@@ -371,186 +370,210 @@ const saveReadingBookProgressToBrowser = (index, pos) => {
* 注意不用监听点击链接导航变化 不对Safari<14.5兼容处理
**/
const onVisibilityChange = () => {
if (document.visibilityState == "hidden") {
API.saveBookProgressWithBeacon(bookProgress.value);
const _bookProgress = bookProgress.value
if (document.visibilityState == 'hidden' && _bookProgress) {
API.saveBookProgressWithBeacon(_bookProgress)
}
};
}
// 定时同步
// 章节切换
const toNextChapter = () => {
store.setContentLoading(true);
let index = chapterIndex.value + 1;
if (typeof catalog.value[index] !== "undefined") {
store.setContentLoading(true)
const index = chapterIndex.value + 1
if (typeof catalog.value[index] !== 'undefined') {
ElMessage({
message: "下一章",
type: "info",
});
getContent(index);
store.saveBookProgress();
message: '下一章',
type: 'info',
})
getContent(index)
store.saveBookProgress()
} else {
ElMessage({
message: "本章是最后一章",
type: "error",
});
message: '本章是最后一章',
type: 'error',
})
}
};
}
const toPreChapter = () => {
store.setContentLoading(true);
let index = chapterIndex.value - 1;
if (typeof catalog.value[index] !== "undefined") {
store.setContentLoading(true)
const index = chapterIndex.value - 1
if (typeof catalog.value[index] !== 'undefined') {
ElMessage({
message: "上一章",
type: "info",
});
getContent(index);
store.saveBookProgress();
message: '上一章',
type: 'info',
})
getContent(index)
store.saveBookProgress()
} else {
ElMessage({
message: "本章是第一章",
type: "error",
});
message: '本章是第一章',
type: 'error',
})
}
};
}
let canJump = true;
let canJump = true
// 监听方向键
const handleKeyPress = (event) => {
if (!canJump) return;
const handleKeyPress = (event: KeyboardEvent) => {
if (!canJump) return
switch (event.key) {
case "ArrowLeft":
event.stopPropagation();
event.preventDefault();
toPreChapter();
break;
case "ArrowRight":
event.stopPropagation();
event.preventDefault();
toNextChapter();
break;
case "ArrowUp":
event.stopPropagation();
event.preventDefault();
case 'ArrowLeft':
event.stopPropagation()
event.preventDefault()
toPreChapter()
break
case 'ArrowRight':
event.stopPropagation()
event.preventDefault()
toNextChapter()
break
case 'ArrowUp':
event.stopPropagation()
event.preventDefault()
if (document.documentElement.scrollTop === 0) {
ElMessage.warning("已到达页面顶部");
ElMessage.warning('已到达页面顶部')
} else {
canJump = false;
canJump = false
jump(0 - document.documentElement.clientHeight + 100, {
duration: store.config.jumpDuration,
callback: () => (canJump = true),
});
})
}
break;
case "ArrowDown":
event.stopPropagation();
event.preventDefault();
break
case 'ArrowDown':
event.stopPropagation()
event.preventDefault()
if (
document.documentElement.clientHeight +
document.documentElement.scrollTop ===
document.documentElement.scrollHeight
) {
ElMessage.warning("已到达页面底部");
ElMessage.warning('已到达页面底部')
} else {
canJump = false;
canJump = false
jump(document.documentElement.clientHeight - 100, {
duration: store.config.jumpDuration,
callback: () => (canJump = true),
});
})
}
break;
break
}
};
}
// 阻止默认滚动事件
const ignoreKeyPress = (event) => {
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
event.preventDefault();
event.stopPropagation();
const ignoreKeyPress = (event: {
key: string
preventDefault: () => void
stopPropagation: () => void
}) => {
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
event.preventDefault()
event.stopPropagation()
}
};
}
onMounted(() => {
//获取书籍数据
let bookUrl = sessionStorage.getItem("bookUrl");
let bookName = sessionStorage.getItem("bookName");
let bookAuthor = sessionStorage.getItem("bookAuthor");
let chapterIndex = Number(sessionStorage.getItem("chapterIndex") || 0);
let chapterPos = Number(sessionStorage.getItem("chapterPos") || 0);
var book = JSON.parse(localStorage.getItem(bookUrl));
const bookUrl = sessionStorage.getItem('bookUrl')
const name = sessionStorage.getItem('bookName')
const author = sessionStorage.getItem('bookAuthor')
const chapterIndex = Number(sessionStorage.getItem('chapterIndex') || 0)
const chapterPos = Number(sessionStorage.getItem('chapterPos') || 0)
const isSeachBook = sessionStorage.getItem('isSeachBook') === 'true'
if (isNullOrBlank(bookUrl) || isNullOrBlank(name) || isNullOrBlank(author)) {
ElMessage.warning('书籍信息为空,即将自动返回书架页面...')
return setTimeout(toShelf, 500)
}
const book: typeof store.readingBook = {
// @ts-expect-error: bookUrl name author is NON_Blank string here
bookUrl,
// @ts-expect-error: bookUrl name author is NON_Blank string here
name,
// @ts-expect-error: bookUrl name author is NON_Blank string here
author,
chapterIndex,
chapterPos,
isSeachBook,
}
/* const bookStr = localStorage.getItem(bookUrl);
if (isNullOrBlank(bookStr)) {
return setTimeout(toShelf, 500);
}
book = JSON.parse(bookStr as string);
if (
book == null ||
chapterIndex != book.index ||
chapterIndex != book.chapterIndex ||
chapterPos != book.chapterPos
) {
book = {
bookName: bookName,
bookAuthor: bookAuthor,
bookUrl: bookUrl,
index: chapterIndex,
chapterPos: chapterPos,
name: bookName!!,
author: bookAuthor!!,
bookUrl,
chapterIndex,
chapterPos,
isSeachBook
};
localStorage.setItem(bookUrl, JSON.stringify(book));
}
onResize();
window.addEventListener("resize", onResize);
} */
onResize()
window.addEventListener('resize', onResize)
loadingWrapper(
API.getChapterList(bookUrl).then(
(res) => {
API.getChapterList(bookUrl as string).then(
res => {
if (!res.data.isSuccess) {
ElMessage({ message: res.data.errorMsg, type: "error" });
setTimeout(toShelf, 500);
return;
ElMessage({ message: res.data.errorMsg, type: 'error' })
setTimeout(toShelf, 500)
return
}
let data = res.data.data;
store.setCatalog(data);
store.setReadingBook(book);
const data = res.data.data
store.setCatalog(data)
store.setReadingBook(book)
getContent(chapterIndex, true, chapterPos);
window.addEventListener("keyup", handleKeyPress);
window.addEventListener("keydown", ignoreKeyPress);
getContent(chapterIndex, true, chapterPos)
window.addEventListener('keyup', handleKeyPress)
window.addEventListener('keydown', ignoreKeyPress)
// 兼容Safari < 14
document.addEventListener("visibilitychange", onVisibilityChange);
document.addEventListener('visibilitychange', onVisibilityChange)
//监听底部加载
scrollObserver = new IntersectionObserver(onReachBottom, {
rootMargin: "-100% 0% 20% 0%",
});
infiniteLoading.value && scrollObserver.observe(loading.value);
rootMargin: '-100% 0% 20% 0%',
})
if (infiniteLoading.value === true)
scrollObserver.observe(loading.value)
//第二次点击同一本书 页面标题不会变化
document.title = null;
document.title = bookName + " | " + catalog.value[chapterIndex].title;
document.title = '...'
document.title =
(name as string) + ' | ' + catalog.value[chapterIndex].title
},
(err) => {
ElMessage({ message: "获取书籍目录失败", type: "error" });
throw err;
err => {
ElMessage({ message: '获取书籍目录失败', type: 'error' })
throw err
},
),
);
});
)
})
onUnmounted(() => {
window.removeEventListener("keyup", handleKeyPress);
window.removeEventListener("keydown", ignoreKeyPress);
window.removeEventListener("resize", onResize);
window.removeEventListener('keyup', handleKeyPress)
window.removeEventListener('keydown', ignoreKeyPress)
window.removeEventListener('resize', onResize)
// 兼容Safari < 14
document.removeEventListener("visibilitychange", onVisibilityChange);
readSettingsVisible.value = false;
popCataVisible.value = false;
scrollObserver?.disconnect();
scrollObserver = null;
});
document.removeEventListener('visibilitychange', onVisibilityChange)
readSettingsVisible.value = false
popCataVisible.value = false
scrollObserver?.disconnect()
scrollObserver = null
})
const addToBookShelfConfirm = async () => {
const bookUrl = sessionStorage.getItem("bookUrl");
const bookName = sessionStorage.getItem("bookName");
const isSeachBook = sessionStorage.getItem("isSeachBook");
const book = JSON.parse(localStorage.getItem(bookUrl));
sessionStorage.removeItem("isSeachBook");
const book = store.readingBook
// 阅读的是搜索的书籍 并未在书架
if (isSeachBook === "true") {
await ElMessageBox.confirm(`是否将《${bookName}》放入书架?`, "放入书架", {
confirmButtonText: "确认",
cancelButtonText: "否",
type: "info",
if (book.isSeachBook === true) {
await ElMessageBox.confirm(`是否将《${book.name}》放入书架?`, '放入书架', {
confirmButtonText: '确认',
cancelButtonText: '否',
type: 'info',
/*
ElMessageBox.confirm默认在触发hashChange事件时自动关闭
按下物理返回键时触发hashChange事件
@@ -560,20 +583,22 @@ const addToBookShelfConfirm = async () => {
})
.then(() => {
//选择是,无动作
isSeachBook.value = false
})
.catch(async () => {
//选择否,删除书籍
await API.deleteBook(book);
});
await API.deleteBook(book)
})
.finally(() => sessionStorage.removeItem('isSeachBook'))
}
};
}
onBeforeRouteLeave(async (to, from, next) => {
console.log("onBeforeRouteLeave");
console.log('onBeforeRouteLeave')
// 弹窗时停止响应按键翻页
window.removeEventListener("keyup", handleKeyPress);
await addToBookShelfConfirm();
next();
});
window.removeEventListener('keyup', handleKeyPress)
await addToBookShelfConfirm()
next()
})
</script>
<style lang="scss" scoped>
@@ -662,8 +687,8 @@ onBeforeRouteLeave(async (to, from, next) => {
}
.chapter {
font-family: "Microsoft YaHei", PingFangSC-Regular, HelveticaNeue-Light,
"Helvetica Neue Light", sans-serif;
font-family: 'Microsoft YaHei', PingFangSC-Regular, HelveticaNeue-Light,
'Helvetica Neue Light', sans-serif;
text-align: left;
padding: 0 65px;
min-height: 100vh;
@@ -673,8 +698,8 @@ onBeforeRouteLeave(async (to, from, next) => {
.content {
font-size: 18px;
line-height: 1.8;
font-family: "Microsoft YaHei", PingFangSC-Regular, HelveticaNeue-Light,
"Helvetica Neue Light", sans-serif;
font-family: 'Microsoft YaHei', PingFangSC-Regular, HelveticaNeue-Light,
'Helvetica Neue Light', sans-serif;
.bottom-bar,
.top-bar {
+264 -296
View File
@@ -27,7 +27,7 @@
size="large"
@click="
toDetail(
readingRecent.url,
readingRecent.bookUrl,
readingRecent.name,
readingRecent.author,
readingRecent.chapterIndex,
@@ -36,7 +36,7 @@
true,
)
"
:class="{ 'no-point': readingRecent.url == '' }"
:class="{ 'no-point': readingRecent.bookUrl == '' }"
>
{{ readingRecent.name }}
</el-tag>
@@ -78,309 +78,277 @@
</div>
</template>
<script>
import "@/assets/bookshelf.css";
import "@/assets/fonts/shelffont.css";
import { useBookStore } from "@/store";
import githubUrl from "@/assets/imgs/github.png";
import { useLoading } from "@/hooks/loading";
import { Search as SearchIcon } from "@element-plus/icons-vue";
import {baseURL_localStorage_key} from "@/api/axios"
<script setup lang="ts">
import '@/assets/bookshelf.css'
import '@/assets/fonts/shelffont.css'
import { useBookStore } from '@/store'
import githubUrl from '@/assets/imgs/github.png'
import { useLoading } from '@/hooks/loading'
import { Search as SearchIcon } from '@element-plus/icons-vue'
import { baseURL_localStorage_key } from '@/api/axios'
import API, {
legado_http_entry_point,
validatorHttpUrl,
setLeagdoHttpUrl,
} from "@api";
parseLeagdoHttpUrlWithDefault,
setApiEntryPoint,
} from '@api'
import { validatorHttpUrl } from '@/utils/utils'
import type { Book, SeachBook } from '@/book'
import type { webReadConfig } from '@/web'
export default defineComponent({
beforeRouteEnter: (to, from, next) => {
API.getReadConfig()
.then((response) => response.data)
.then(({ isSuccess, data }) => {
if (isSuccess) {
next((vm) => {
console.log("初始化加载阅读界面配置成功");
// @ts-ignore
vm.saveReadConfig(data);
});
} else {
next();
}
})
.catch(() => next());
},
setup: () => {
const store = useBookStore();
const isNight = computed(() => store.isNight);
const store = useBookStore()
const isNight = computed(() => store.isNight)
const readingRecent = ref({
name: "尚无阅读记录",
author: "",
url: "",
chapterIndex: 0,
chapterPos: 0,
isSeachBook: false,
});
const shelfWrapper = ref(null);
const { showLoading, closeLoading, loadingWrapper, isLoading } = useLoading(
shelfWrapper,
"正在获取书籍信息",
);
/** shortcuts of `store.setConfig` */
const applyReadConfig = (config?: webReadConfig) => {
try {
if (config !== undefined) store.setConfig(config)
} catch {
ElMessage.info('阅读界面配置解析错误')
}
}
// 书架书籍和在线书籍搜索
const books = shallowRef([]);
const shelf = computed(() => store.shelf);
const searchWord = ref("");
const isSearching = ref(false);
watchEffect(() => {
if (isSearching.value && searchWord.value != "") return;
isSearching.value = false;
books.value = [];
if (searchWord.value == "") {
books.value = shelf.value;
return;
const readingRecent = ref<typeof store.readingBook>({
name: '尚无阅读记录',
author: '',
bookUrl: '',
chapterIndex: 0,
chapterPos: 0,
isSeachBook: false,
})
const shelfWrapper = ref<HTMLElement>()
//const shelfWrapper = useTemplateRef<HTMLElement>("shelfWrapper")
const { showLoading, closeLoading, loadingWrapper, isLoading } = useLoading(
shelfWrapper,
'正在获取书籍信息',
)
// 书架书籍和在线书籍搜索
const books = shallowRef<Book[] | SeachBook[]>([])
const shelf = computed(() => store.shelf)
const searchWord = ref('')
const isSearching = ref(false)
watchEffect(() => {
if (isSearching.value && searchWord.value != '') return
isSearching.value = false
books.value = []
if (searchWord.value == '') {
books.value = shelf.value
return
}
books.value = shelf.value.filter(book => {
return (
book.name.includes(searchWord.value) ||
book.author.includes(searchWord.value)
)
})
})
//搜索在线书籍
const searchBook = () => {
if (searchWord.value == '') return
books.value = []
store.clearSearchBooks()
showLoading()
isSearching.value = true
API.search(
searchWord.value,
searcBooks => {
if (isLoading) {
closeLoading()
}
books.value = shelf.value.filter((book) => {
return (
book.name.includes(searchWord.value) ||
book.author.includes(searchWord.value)
);
});
});
//搜索在线书籍
const searchBook = () => {
if (searchWord.value == "") return;
books.value = [];
store.clearSearchBooks();
showLoading();
isSearching.value = true;
API.search(
searchWord.value,
(data) => {
if (isLoading) {
closeLoading();
}
try {
store.setSearchBooks(JSON.parse(data));
books.value = store.searchBooks;
//store.searchBooks.forEach((item) => books.value.push(item));
} catch (e) {
ElMessage.error("后端数据错误");
throw e;
}
},
() => {
closeLoading();
if (books.value.length == 0) {
ElMessage.info("搜索结果为空");
}
},
);
};
//连接状态
const connectStatus = computed(() => store.connectStatus);
const connectType = computed(() => store.connectType);
const newConnect = computed(() => store.newConnect);
const setLegadoRetmoteUrl = () => {
ElMessageBox.prompt(
"请输入 后端地址 ( 如:http://127.0.0.1:9527 或者通过内网穿透的地址)",
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
inputPlaceholder: legado_http_entry_point,
inputValidator: (value) => {
try {
validatorHttpUrl(value);
} catch (e) {
return e?.cause?.message ?? e.message;
}
return true;
},
beforeClose: (action, instance, done) => {
if (action === "confirm") {
store.setNewConnect(true);
instance.confirmButtonLoading = true;
instance.confirmButtonText = "校验中……";
// instance.inputValue
const url = new URL(instance.inputValue).toString();
API.testLeagdoHttpUrlConnection(url)
//API.getBookShelf()
.then(function (configStr) {
saveReadConfig(configStr);
instance.confirmButtonLoading = false;
store.setConnectType("success");
store.clearSearchBooks();
store.setNewConnect(false);
setLeagdoHttpUrl(url);
if (url === location.origin) {
localStorage.removeItem(baseURL_localStorage_key);
} else {
localStorage.setItem(baseURL_localStorage_key, url);
}
store.setConnectStatus("已连接 " + url.toString());
fetchBookShelfData();
done();
})
.catch(function (error) {
instance.confirmButtonLoading = false;
instance.confirmButtonText = "确定";
ElMessage.error("访问失败,请检查您输入的 url");
store.setNewConnect(false);
throw error;
});
} else {
done();
}
},
},
);
};
const router = useRouter();
const handleBookClick = async (book) => {
const {
bookUrl,
name,
author,
durChapterIndex = 0,
durChapterPos = 0,
} = book;
// 判断是否为 searchBook
const isSeachBook = "respondTime" in book;
if (isSeachBook) {
await API.saveBook(book);
}
toDetail(
bookUrl,
name,
author,
durChapterIndex,
durChapterPos,
isSeachBook,
);
};
const toDetail = (
bookUrl,
bookName,
bookAuthor,
chapterIndex,
chapterPos,
isSeachBook,
fromReadRecentClick = false,
) => {
if (bookName === "尚无阅读记录") return;
// 最近书籍不再书架上 自动搜索
if (isSeachBook === true && fromReadRecentClick) {
searchWord.value = bookName;
searchBook();
return;
}
sessionStorage.setItem("bookUrl", bookUrl);
sessionStorage.setItem("bookName", bookName);
sessionStorage.setItem("bookAuthor", bookAuthor);
sessionStorage.setItem("chapterIndex", chapterIndex);
sessionStorage.setItem("chapterPos", chapterPos);
sessionStorage.setItem("isSeachBook", String(isSeachBook));
readingRecent.value = {
name: bookName,
author: bookAuthor,
url: bookUrl,
chapterIndex: chapterIndex,
chapterPos: chapterPos,
isSeachBook,
};
localStorage.setItem(
"readingRecent",
JSON.stringify(readingRecent.value),
);
router.push({
path: "/chapter",
});
};
const loadShelf = () => {
loadingWrapper(
store
.saveBookProgress()
//确保各种网络情况下同步请求先完成
.finally(fetchBookShelfData),
);
};
const saveReadConfig = (configStr) => {
try {
store.setConfig(JSON.parse(configStr));
} catch {
ElMessage.info("阅读界面配置解析错误");
store.setSearchBooks(searcBooks)
books.value = store.searchBooks
//store.searchBooks.forEach((item) => books.value.push(item));
} catch (e) {
ElMessage.error('后端数据错误')
throw e
}
};
},
() => {
closeLoading()
if (books.value.length == 0) {
ElMessage.info('搜索结果为空')
}
},
)
}
const fetchBookShelfData = () => {
return API.getBookShelf().then((response) => {
store.setConnectType("success");
if (response.data.isSuccess) {
//store.increaseBookNum(response.data.data.length);
store.addBooks(
response.data.data.sort(function (a, b) {
var x = a["durChapterTime"] || 0;
var y = b["durChapterTime"] || 0;
return y - x;
}),
);
//连接状态
const connectStatus = computed(() => store.connectStatus)
const connectType = computed(() => store.connectType)
const newConnect = computed(() => store.newConnect)
const setLegadoRetmoteUrl = () => {
ElMessageBox.prompt(
'请输入 后端地址 ( 如:http://127.0.0.1:9527 或者通过内网穿透的地址)',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPlaceholder: legado_http_entry_point,
inputValidator: value => validatorHttpUrl(value),
inputErrorMessage: '输入的格式不对',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
store.setNewConnect(true)
instance.confirmButtonLoading = true
instance.confirmButtonText = '校验中……'
// instance.inputValue
const url = new URL(instance.inputValue).toString()
API.getReadConfig(url)
//API.getBookShelf()
.then(function (config) {
applyReadConfig(config)
instance.confirmButtonLoading = false
store.setConnectType('success')
store.clearSearchBooks()
store.setNewConnect(false)
setApiEntryPoint(...parseLeagdoHttpUrlWithDefault(url))
if (url === location.origin) {
localStorage.removeItem(baseURL_localStorage_key)
} else {
localStorage.setItem(baseURL_localStorage_key, url)
}
store.setConnectStatus('已连接 ' + url.toString())
fetchBookShelfData()
done()
})
.catch(function (error) {
instance.confirmButtonLoading = false
instance.confirmButtonText = '确定'
ElMessage.error('访问失败,请检查您输入的 url')
store.setNewConnect(false)
throw error
})
} else {
ElMessage.error(response.data.errorMsg ?? "后端返回格式错误!");
}
store.setConnectStatus("已连接 " + legado_http_entry_point);
store.setNewConnect(false);
});
};
onMounted(() => {
//获取最近阅读书籍
let readingRecentStr = localStorage.getItem("readingRecent");
if (readingRecentStr != null) {
readingRecent.value = JSON.parse(readingRecentStr);
if (typeof readingRecent.value.chapterIndex == "undefined") {
readingRecent.value.chapterIndex = 0;
done()
}
},
},
)
}
const router = useRouter()
const handleBookClick = async (book: SeachBook | Book) => {
// 判断是否为 searchBook
const isSeachBook = 'respondTime' in book
if (isSeachBook) {
await API.saveBook(book)
}
const {
bookUrl,
name,
author,
// @ts-expect-error: descruct with default value
durChapterIndex = 0,
// @ts-expect-error: descruct with default value
durChapterPos = 0,
} = book
toDetail(bookUrl, name, author, durChapterIndex, durChapterPos, isSeachBook)
}
const toDetail = (
bookUrl: string,
bookName: string,
bookAuthor: string,
chapterIndex: number,
chapterPos: number,
isSeachBook: boolean | undefined = false,
fromReadRecentClick = false,
) => {
if (bookName === '尚无阅读记录') return
// 最近书籍不再书架上 自动搜索
if (
fromReadRecentClick &&
shelf.value.every(book => book.bookUrl !== bookUrl)
) {
searchWord.value = bookName
searchBook()
return
}
sessionStorage.setItem('bookUrl', bookUrl)
sessionStorage.setItem('bookName', bookName)
sessionStorage.setItem('bookAuthor', bookAuthor)
sessionStorage.setItem('chapterIndex', String(chapterIndex))
sessionStorage.setItem('chapterPos', String(chapterPos))
sessionStorage.setItem('isSeachBook', String(isSeachBook))
readingRecent.value = {
name: bookName,
author: bookAuthor,
bookUrl,
chapterIndex,
chapterPos,
isSeachBook,
}
localStorage.setItem('readingRecent', JSON.stringify(readingRecent.value))
router.push({
path: '/chapter',
})
}
const loadShelf = async () => {
try {
if (store.configInited === false) {
const config = await API.getReadConfig()
applyReadConfig(config)
} else {
}
await store.saveBookProgress()
//确保各种网络情况下同步请求先完成
await fetchBookShelfData()
} catch (error: unknown) {
store.setConnectType('danger')
store.setConnectStatus('连接异常')
store.setNewConnect(false)
throw error
}
}
const fetchBookShelfData = () => {
return API.getBookShelf().then(response => {
store.setConnectType('success')
if (response.data.isSuccess) {
//store.increaseBookNum(response.data.data.length);
store.addBooks(
response.data.data.sort(function (a, b) {
const x = a['durChapterTime'] || 0
const y = b['durChapterTime'] || 0
return y - x
}),
)
} else {
if (
response.data.errorMsg.includes('还没有添加小说') &&
shelf.value.length > 0
) {
ElNotification.warning({
title: '提示',
message: '当前书架上的书籍已经被删除',
position: 'bottom-right',
})
return store.clearBooks()
}
console.log("bookshelf mounted");
API.testLeagdoHttpUrlConnection()
//.then(saveReadConfig) 应该在组件挂载前读取阅读配置
.then(loadShelf)
.catch(function (error) {
store.setConnectType("danger");
store.setConnectStatus("连接异常");
ElMessage.error(
"后端连接失败异常,请检查阅读WEB服务或者设置其它可用链接",
);
store.setNewConnect(false);
throw error;
});
});
return {
setLegadoRetmoteUrl,
isNight,
connectStatus,
connectType,
newConnect,
saveReadConfig, //expose it so beforeRouteEnter next can access it
readingRecent,
searchBook,
books,
handleBookClick,
toDetail,
isSearching,
SearchIcon,
githubUrl,
searchWord,
};
},
});
ElMessage.error(response.data.errorMsg ?? '后端返回格式错误!')
}
store.setConnectStatus('已连接 ' + legado_http_entry_point)
store.setNewConnect(false)
})
}
onMounted(() => {
//获取最近阅读书籍
const readingRecentStr = localStorage.getItem('readingRecent')
if (readingRecentStr != null) {
readingRecent.value = JSON.parse(readingRecentStr)
if (typeof readingRecent.value.chapterIndex == 'undefined') {
readingRecent.value.chapterIndex = 0
}
}
console.log('bookshelf mounted')
loadingWrapper(loadShelf())
})
</script>
<style lang="scss" scoped>
<style scoped>
.index-wrapper {
height: 100%;
width: 100%;
@@ -438,12 +406,12 @@ export default defineComponent({
.recent-book {
font-size: 10px;
// font-weight: 400;
/* // font-weight: 400;
// margin: 12px 0;
// font-weight: 500;
// color: #6B7C87;
// color: #6B7C87; */
cursor: pointer;
// padding: 6px 18px;
/* // padding: 6px 18px; */
}
}
}
@@ -464,7 +432,7 @@ export default defineComponent({
.setting-connect {
font-size: 8px;
margin-top: 16px;
// color: #6B7C87;
/* // color: #6B7C87; */
cursor: pointer;
}
}
@@ -538,7 +506,7 @@ export default defineComponent({
}
.night {
:deep(.navigation-wrapper) {
.navigation-wrapper {
background-color: #454545;
.navigation-title {
+12 -11
View File
@@ -5,22 +5,23 @@
<source-tab-tools class="right" />
</div>
</template>
<script setup>
import bookSourceConfig from "@/config/bookSourceEditConfig";
import rssSourceConfig from "@/config/rssSourceEditConfig";
import "@/assets/sourceeditor.css";
import { useDark } from "@vueuse/core";
<script setup lang="ts">
import bookSourceConfig from '@/config/bookSourceEditConfig'
import rssSourceConfig from '@/config/rssSourceEditConfig'
import '@/assets/sourceeditor.css'
import { useDark } from '@vueuse/core'
import type { SourceConfig } from '@/config/sourceConfig'
useDark();
useDark()
let config;
let config: SourceConfig
if (/bookSource/i.test(location.href)) {
config = bookSourceConfig;
document.title = "书源管理";
config = bookSourceConfig as SourceConfig
document.title = '书源管理'
} else {
config = rssSourceConfig;
document.title = "订阅源管理";
config = rssSourceConfig as SourceConfig
document.title = '订阅源管理'
}
</script>
<style lang="scss" scoped>