文字说明
采用vue3实现的简单的小说阅读器,导入小说的txt文本,即可进行阅读;目前对于面板设置以及翻页的效果实现的并不好;可以等后续有新思路了来进行完善
采用indexDB存储上传的文本数据
可利用Hbuilder的5+APP将源码打包为手机APP
核心代码
列表页面源码
<script setup>
import {onBeforeMount, reactive, ref} from "vue";
import {confirm, loading, message} from "@/util";
import {openBookContentLog} from "@/util/config";
import {dbOperation} from "@/util/dbOperation";
import {useRouter} from "vue-router";
import jschardet from 'jschardet';
const data = reactive({
bookForm: {
bookName: "",
bookContent: "",
bookBrief: "",
},
bookRules: {
bookName: [{required: true, message: '请填写书籍名称', trigger: 'blur'}],
bookContent: [{required: true, message: '请上传书籍内容', trigger: 'blur'}],
bookBrief: [{required: true, message: '请填写书籍简介', trigger: 'blur'}],
},
bookUploadVisible: false,
bookList: [],
});
onBeforeMount(() => {
initBookList();
});
function initBookList() {
const bookList = localStorage.getItem("bookList");
if (bookList) {
data.bookList = JSON.parse(bookList);
}
}
function openUpload() {
data.bookForm.bookName = "";
data.bookForm.bookContent = "";
data.bookForm.bookBrief = "";
data.bookUploadVisible = true;
}
function cancelUpload() {
confirm("关闭后导入信息会被清空,是否继续?", () => {
data.bookUploadVisible = false;
});
}
function uploadFile(event) {
const file = event.target.files[0];
if (!file) {
return;
}
const loadingInstance = loading("导入中...");
setTimeout(() => {
getEncoding(file, (encoding) => {
const reader = new FileReader();
reader.readAsText(file, encoding);
reader.onload = (e) => {
loadingInstance.close();
const bookContent = e.target.result;
if (!bookContent) {
message("导出书籍内容为空", "warning");
return;
}
data.bookForm.bookContent = bookContent;
bookFormRef.value.validateField("bookContent");
}
});
}, 100);
}
function getEncoding(file, success) {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (e) => {
const dataUrl = e.target.result;
const encoding = jschardet.detect(atob(dataUrl.split(';base64,')[1]));
success(encoding.encoding);
}
}
const bookFormRef = ref();
function uploadBook() {
bookFormRef.value.validate(async (valid) => {
if (valid) {
const loadingInstance = loading("数据保存中...");
const book_id = Date.now();
data.bookList.push({
book_id: book_id,
book_name: data.bookForm.bookName,
book_brief: data.bookForm.bookBrief,
});
localStorage.setItem("bookList", JSON.stringify(data.bookList));
await openBookContentLog();
await dbOperation.add([{
book_id: book_id,
book_content: data.bookForm.bookContent,
}]);
loadingInstance.close();
data.bookUploadVisible = false;
initBookList();
}
});
}
function moveIndex(item, step) {
let i;
for (i = 0; i < data.bookList.length; i++) {
if (item.book_id === data.bookList[i].book_id) {
break;
}
}
if (step === 0) {
if (i !== 0) {
data.bookList.splice(i, 1);
data.bookList.unshift(item);
localStorage.setItem("bookList", JSON.stringify(data.bookList));
}
} else if (step === 1) {
if (i < data.bookList.length - 1) {
data.bookList.splice(i, 1);
data.bookList.splice(i + 1, 0, item);
localStorage.setItem("bookList", JSON.stringify(data.bookList));
}
} else if (step === -1) {
if (i > 0) {
data.bookList.splice(i, 1);
data.bookList.splice(i - 1, 0, item);
localStorage.setItem("bookList", JSON.stringify(data.bookList));
}
}
}
function deleteBook(item) {
confirm("确认删除该书籍吗?", async () => {
let i;
for (i = 0; i < data.bookList.length; i++) {
if (item.book_id === data.bookList[i].book_id) {
break;
}
}
data.bookList.splice(i, 1);
localStorage.setItem("bookList", JSON.stringify(data.bookList));
await openBookContentLog();
const res = await dbOperation.getDataByField("book_id", item.book_id);
if (res.data.length > 0) {
await dbOperation.delete([res.data[0].id]);
}
});
}
const router = useRouter();
function read(item) {
router.push({
path: "/bookRead",
query: {
book_id: item.book_id,
book_name: item.book_name,
}
});
}
</script>
<template>
<div class="container">
<el-button type="danger" @click="openUpload">导入</el-button>
<template v-for="item in data.bookList" :key="item.book_id">
<el-card style="margin: 1rem 0">
<h3>{{ item.book_name }}</h3>
<p style="margin-top: 5px">{{ item.book_brief }}</p>
<template #footer>
<div style="display: flex; align-items: center; justify-content: center">
<el-button type="info" @click="read(item)">阅读</el-button>
<el-button type="info" @click="deleteBook(item)">删除</el-button>
<el-button type="danger" @click="moveIndex(item, 0)">置顶</el-button>
<el-button type="primary" @click="moveIndex(item, -1)">上移</el-button>
<el-button type="primary" @click="moveIndex(item, 1)">下移</el-button>
</div>
</template>
</el-card>
</template>
</div>
<el-dialog v-model="data.bookUploadVisible" :before-close="cancelUpload" title="导入书籍" width="90%">
<el-form ref="bookFormRef" :model="data.bookForm" :rules="data.bookRules" label-position="right"
label-width="auto">
<el-form-item label="名称:" prop="bookName">
<el-input v-model="data.bookForm.bookName"/>
</el-form-item>
<el-form-item label="导入:" prop="bookContent">
<input v-if="data.bookUploadVisible" accept=".txt" type="file" @change="uploadFile($event)">
</el-form-item>
<el-form-item label="简介:" prop="bookBrief">
<el-input v-model="data.bookForm.bookBrief" :rows="15" resize="none" type="textarea"/>
</el-form-item>
</el-form>
<template #footer>
<el-button type="info" @click="cancelUpload">取消</el-button>
<el-button type="danger" @click="uploadBook">保存</el-button>
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
</style>
阅读页面源码
<script setup>
import {useRoute, useRouter} from "vue-router";
import {computed, nextTick, onBeforeMount, reactive} from "vue";
import {dbOperation} from "@/util/dbOperation";
import {openBookContentLog} from "@/util/config";
import {loading} from "@/util";
const data = reactive({
bookContent: "",
bookName: "",
pageIndex: 0,
total: 0,
});
const bookConvertContentList = [];
const gap = 1000;
let book_id;
const route = useRoute();
onBeforeMount(async () => {
const loadingInstance = loading("数据加载中...");
book_id = route.query.book_id;
const book_name = route.query.book_name;
if (book_name) {
data.bookName = book_name;
}
if (book_id) {
const book_id__page_index = localStorage.getItem("book_id__" + book_id);
if (book_id__page_index) {
data.pageIndex = Number(book_id__page_index);
}
await openBookContentLog();
const res = await dbOperation.getDataByField("book_id", Number(book_id));
if (res.data.length > 0) {
const bookContent = res.data[0].book_content;
let index = 0;
while (index < bookContent.length) {
let end = index + gap;
const nextSplit = bookContent.indexOf("\r\n", end);
if (nextSplit !== -1) {
end = nextSplit;
}
const substr = bookContent.substring(index, end);
const bookLineList = substr.split("\r\n");
let bookConvertContent = "";
for (let i = 0; i < bookLineList.length; i++) {
bookConvertContent += `<p>${bookLineList[i]}</p><br/>`;
}
bookConvertContentList.push(bookConvertContent);
index = end;
}
data.bookContent = bookConvertContentList[data.pageIndex];
data.total = bookConvertContentList.length;
loadingInstance.close();
}
}
});
const router = useRouter();
function goBack() {
router.push({
path: "/bookList",
});
}
function lastPage() {
if (data.pageIndex > 0) {
data.pageIndex--;
localStorage.setItem("book_id__" + book_id, data.pageIndex);
data.bookContent = bookConvertContentList[data.pageIndex];
nextTick(() => {
const container = document.getElementsByClassName("container")[0];
container.scrollTo(0, 0);
});
}
}
function nextPage() {
if (data.pageIndex < bookConvertContentList.length - 1) {
data.pageIndex++;
localStorage.setItem("book_id__" + book_id, data.pageIndex);
data.bookContent = bookConvertContentList[data.pageIndex];
nextTick(() => {
const container = document.getElementsByClassName("container")[0];
container.scrollTo(0, 0);
});
}
}
const bottomTip = computed(() => {
return (data.pageIndex + 1) + " / " + data.total;
});
</script>
<template>
<div class="container">
<el-page-header :content="data.bookName" @back="goBack"></el-page-header>
<div style="padding: 1rem 0" v-html="data.bookContent"></div>
<div style="display: flex; align-items: center; justify-content: center">
<el-page-header style="width: fit-content" title="Last" @back="lastPage"></el-page-header>
<div style="flex: 1; text-align: center; font-size: 18px">{{ bottomTip }}</div>
<el-page-header class="next" style="transform: rotate(180deg); width: fit-content" title="Next"
@back="nextPage"></el-page-header>
</div>
</div>
</template>
<style lang="scss">
.next {
.el-page-header__title, .el-page-header__content {
transform: rotate(180deg) !important;
}
}
</style>
效果展示
书籍列表
阅读页面
翻页