modules 添加web

This commit is contained in:
Xwite
2023-04-07 20:28:21 +08:00
parent ed14659aa6
commit 95f53948f1
88 changed files with 5784 additions and 0 deletions
+184
View File
@@ -0,0 +1,184 @@
<template>
<div class="books-wrapper">
<div class="wrapper">
<div
class="book"
v-for="book in props.books"
:key="book.noteUrl"
@click="handleClick(book)"
>
<div class="cover-img">
<img
class="cover"
:src="getCover(book.coverUrl)"
:key="book.coverUrl"
alt=""
loading="lazy"
/>
</div>
<div class="info">
<div class="name">{{ book.name }}</div>
<div class="sub">
<div class="author">
{{ book.author }}
</div>
<div class="tags" v-show="props.isSearch">
<el-tag
v-for="tag in book.kind.split(',').slice(0, 2)"
:key="tag"
>
{{ tag }}
</el-tag>
</div>
<div class="update-info" v-show="!props.isSearch">
<div class="dot"></div>
<div class="size">{{ book.totalChapterNum }}</div>
<div class="dot"></div>
<div class="date">{{ dateFormat(book.lastCheckTime) }}</div>
</div>
</div>
<div class="intro" v-show="props.isSearch">{{ book.intro }}</div>
<div class="dur-chapter" v-show="!props.isSearch">
已读{{ book.durChapterTitle }}
</div>
<div class="last-chapter">最新{{ book.latestChapterTitle }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { dateFormat } from "../plugins/utils";
const props = defineProps(["books", "isSearch"]);
const emit = defineEmits(["bookClick"]);
const handleClick = (book) => emit("bookClick", toRaw(book));
const getCover = (coverUrl) => {
return /^data:/.test(coverUrl)
? coverUrl
: (import.meta.env.VITE_API || location.origin) +
"/cover?path=" +
encodeURIComponent(coverUrl);
};
const subJustify = computed(() =>
props.isSearch ? "space-between" : "flex-start"
);
</script>
<style lang="scss" scoped>
.books-wrapper {
overflow: scroll;
.wrapper {
display: grid;
grid-template-columns: repeat(auto-fill, 380px);
justify-content: space-around;
grid-gap: 10px;
.book {
user-select: none;
display: flex;
cursor: pointer;
margin-bottom: 18px;
padding: 24px 24px;
width: 360px;
flex-direction: row;
justify-content: space-around;
.cover-img {
width: 84px;
height: 112px;
.cover {
width: 84px;
height: 112px;
}
}
.info {
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: left;
height: 112px;
margin-left: 20px;
flex: 1;
.name {
width: fit-content;
font-size: 16px;
font-weight: 700;
color: #33373d;
}
.sub {
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: v-bind("subJustify");
font-size: 12px;
font-weight: 600;
color: #6b6b6b;
.tags {
:deep(.el-tag) {
margin-right: 0.5em;
}
}
.update-info {
display: flex;
.dot {
margin: 0 7px;
}
}
}
.intro,
.dur-chapter,
.last-chapter {
color: #969ba3;
font-size: 13px;
margin-top: 3px;
font-weight: 500;
word-wrap: break-word;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
text-align: left;
}
}
}
.book:hover {
background: rgba(0, 0, 0, 0.1);
transition-duration: 0.5s;
}
}
.wrapper:last-child {
margin-right: auto;
}
}
.books-wrapper::-webkit-scrollbar {
width: 0 !important;
}
@media screen and (max-width: 750px) {
.books-wrapper {
.wrapper {
display: flex;
flex-direction: column;
.book {
box-sizing: border-box;
width: 100%;
margin-bottom: 0;
padding: 10px 20px;
}
}
}
}
</style>
@@ -0,0 +1,61 @@
<template>
<div v-for="(para, index) in props.carray" :key="index">
<img
class="full"
v-if="/^\s*<img[^>]*src[^>]+>$/.test(para)"
:src="getImageSrc(para)"
loading="lazy"
/>
<p v-else :style="style" v-html="para" />
</div>
</template>
<script setup>
import config from "../plugins/config";
const store = useBookStore();
const props = defineProps(["carray"]);
const fontFamily = computed(() => {
if (store.config.font >= 0) {
return config.fonts[store.config.font];
}
return { fontFamily: store.config.customFontName };
});
const fontSize = computed(() => {
return store.config.fontSize + "px";
});
const style = computed(() => {
let style = fontFamily.value;
style.fontSize = fontSize.value;
return style;
});
function getImageSrc(content) {
const imgPattern = /<img[^>]*src="([^"]*(?:"[^>]+\})?)"[^>]*>/;
return content.match(imgPattern)[1];
}
watch(fontSize, () => {
store.setShowContent(false);
nextTick(() => {
store.setShowContent(true);
});
});
</script>
<style lang="scss" scoped>
p {
display: block;
word-wrap: break-word;
word-break: break-all;
:deep(img) {
height: 1em;
}
}
.full {
display: block;
width: 100%;
}
</style>
+137
View File
@@ -0,0 +1,137 @@
<template>
<div class="cata-wrapper" :style="popupTheme">
<div class="title">目录</div>
<div
class="data-wrapper"
ref="cataData"
:class="{ night: isNight, day: !isNight }"
>
<div class="cata">
<div
class="log"
v-for="(note, index) in catalog"
:class="{ selected: isSelected(index) }"
:key="note.durChapterIndex"
@click="gotoChapter(note)"
ref="cata"
>
<div class="log-text">
{{ note.title }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import jump from "../plugins/jump";
import settings from "../plugins/config";
import "../assets/fonts/popfont.css";
const store = useBookStore();
const isNight = ref(false);
const { index } = toRefs(store.readingBook);
const { catalog, popCataVisible } = storeToRefs(store);
const theme = computed(() => {
return store.config.theme;
});
const popupTheme = computed(() => {
return {
background: settings.themes[theme.value].popup,
};
});
watchEffect(() => {
isNight.value = theme.value == 6;
});
const cata = ref();
const cataData = ref();
watch(popCataVisible, () => {
nextTick(() => {
let wrapper = cataData.value;
jump(cata.value[index.value], { container: wrapper, duration: 0 });
});
});
const isSelected = (idx) => {
return idx == index.value;
};
const emit = defineEmits(["getContent"]);
const gotoChapter = (note) => {
index.value = catalog.value.indexOf(note);
store.setPopCataVisible(false);
store.setContentLoading(true);
emit("getContent", index.value);
};
</script>
<style lang="scss" scoped>
.cata-wrapper {
margin: -16px;
padding: 18px 0 24px 25px;
// background: #ede7da url('../assets/imgs/themes/popup_1.png') repeat;
.title {
font-size: 18px;
font-weight: 400;
font-family: FZZCYSK;
margin: 0 0 20px 0;
color: #ed4259;
width: fit-content;
border-bottom: 1px solid #ed4259;
}
.data-wrapper {
height: 300px;
overflow: auto;
.cata {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
.selected {
color: #eb4259;
}
.log {
width: 50%;
height: 40px;
cursor: pointer;
float: left;
font: 16px / 40px PingFangSC-Regular, HelveticaNeue-Light,
"Helvetica Neue Light", "Microsoft YaHei", sans-serif;
.log-text {
margin-right: 26px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
}
.night {
:deep(.log) {
border-bottom: 1px solid #666;
}
}
.day {
:deep(.log) {
border-bottom: 1px solid #f2f2f2;
}
}
}
@media screen and (max-width: 500px) {
.cata-wrapper .data-wrapper .cata .log {
width: 100%;
}
}
</style>
+477
View File
@@ -0,0 +1,477 @@
<template>
<div
class="settings-wrapper"
:style="popupTheme"
:class="{ night: isNight, day: !isNight }"
>
<div class="settings-title">设置</div>
<div class="setting-list">
<ul>
<li class="theme-list">
<i>阅读主题</i>
<span
class="theme-item"
v-for="(themeColor, index) in themeColors"
:key="index"
:style="themeColor"
ref="themes"
@click="setTheme(index)"
:class="{ selected: selectedTheme == index }"
><em v-if="index < 6" class="iconfont">&#58980;</em
><em v-else class="moon-icon">{{ moonIcon }}</em></span
>
</li>
<li class="font-list">
<i>正文字体</i>
<span
class="font-item"
v-for="(font, index) in fonts"
:key="index"
:class="{ selected: selectedFont == index }"
@click="setFont(index)"
>{{ font }}</span
>
</li>
<li class="font-list">
<i>自定字体</i>
<el-tooltip effect="dark" content="自定义的字体名称" placement="top">
<input
type="text"
class="font-item font-item-input"
v-model="customFontName"
placeholder="请输入自定义的字体名称"
/>
</el-tooltip>
<el-popover
placement="top"
width="180"
trigger="click"
v-model:visible="customFontSavePopVisible"
>
<p>
请确认输入的字体名称完整无误并且该字体已经安装在您的设备上
</p>
<p>确定保存吗</p>
<div style="text-align: right; margin: 0">
<el-button
size="small"
plain
@click="customFontSavePopVisible = false"
>取消</el-button
>
<el-button
type="primary"
size="small"
@click="
setCustomFont();
customFontSavePopVisible = false;
"
>确定</el-button
>
</div>
<template #reference>
<span type="text" class="font-item">保存</span>
</template>
</el-popover>
</li>
<li class="font-size">
<i>字体大小</i>
<div class="resize">
<span class="less" @click="lessFontSize"
><em class="iconfont">&#58966;</em></span
><b></b> <span class="lang">{{ fontSize }}</span
><b></b>
<span class="more" @click="moreFontSize"
><em class="iconfont">&#58976;</em></span
>
</div>
</li>
<li class="read-width" v-if="!store.miniInterface">
<i>页面宽度</i>
<div class="resize">
<span class="less" @click="lessReadWidth"
><em class="iconfont">&#58965;</em></span
><b></b> <span class="lang">{{ readWidth }}</span
><b></b>
<span class="more" @click="moreReadWidth"
><em class="iconfont">&#58975;</em></span
>
</div>
</li>
<li class="infinite-loading">
<i>无限加载</i>
<span
class="infinite-loading-item"
:key="0"
:class="{ selected: infiniteLoading == false }"
@click="setInfiniteLoading(false)"
>关闭</span
>
<span
class="infinite-loading-item"
:key="1"
:class="{ selected: infiniteLoading == true }"
@click="setInfiniteLoading(true)"
>开启</span
>
</li>
</ul>
</div>
</div>
</template>
<script setup>
import "../assets/fonts/popfont.css";
import "../assets/fonts/iconfont.css";
import settings from "../plugins/config";
import API from "@api";
const store = useBookStore();
const theme = ref(0);
const isNight = ref(store.config.theme == 6);
const moonIcon = ref("");
const themeColors = shallowRef([
{
background: "rgba(250, 245, 235, 0.8)",
},
{
background: "rgba(245, 234, 204, 0.8)",
},
{
background: "rgba(230, 242, 230, 0.8)",
},
{
background: "rgba(228, 241, 245, 0.8)",
},
{
background: "rgba(245, 228, 228, 0.8)",
},
{
background: "rgba(224, 224, 224, 0.8)",
},
{
background: "rgba(0, 0, 0, 0.5)",
},
]);
const moonIconStyle = ref({
display: "inline",
color: "rgba(255,255,255,0.2)",
});
const fonts = ref(["雅黑", "宋体", "楷书"]);
const customFontName = ref(store.config.customFontName);
const customFontSavePopVisible = ref(false);
onMounted(() => {
//初始化设置项目
var config = store.config;
theme.value = config.theme;
if (theme.value == 6) {
moonIcon.value = "";
} else {
moonIcon.value = "";
}
});
const config = computed(() => {
return store.config;
});
const popupTheme = computed(() => {
return {
background: settings.themes[config.value.theme].popup,
};
});
const selectedTheme = computed(() => {
return store.config.theme;
});
const selectedFont = computed(() => {
return store.config.font;
});
const fontSize = computed(() => {
return store.config.fontSize;
});
const readWidth = computed(() => {
return store.config.readWidth;
});
const infiniteLoading = computed(() => {
return store.config.infiniteLoading;
});
const setTheme = (theme) => {
if (theme == 6) {
isNight.value = true;
moonIcon.value = "";
moonIconStyle.value.color = "#ed4259";
} else {
isNight.value = false;
moonIcon.value = "";
moonIconStyle.value.color = "rgba(255,255,255,0.2)";
}
config.value.theme = theme;
saveConfig(config.value);
};
const setFont = (font) => {
config.value.font = font;
saveConfig(config.value);
};
const setCustomFont = () => {
config.value.font = -1;
config.value.customFontName = customFontName.value;
saveConfig(config.value);
};
const moreFontSize = () => {
if (config.value.fontSize < 48) config.value.fontSize += 2;
saveConfig(config.value);
};
const lessFontSize = () => {
if (config.value.fontSize > 12) config.value.fontSize -= 2;
saveConfig(config.value);
};
const moreReadWidth = () => {
/*if (config.value.readWidth < 960)*/
config.value.readWidth += 160;
saveConfig(config.value);
};
const lessReadWidth = () => {
if (config.value.readWidth > 640) config.value.readWidth -= 160;
saveConfig(config.value);
};
const setInfiniteLoading = (loading) => {
config.value.infiniteLoading = loading;
saveConfig(config.value);
};
const saveConfig = (config) => {
store.setConfig(config);
localStorage.setItem("config", JSON.stringify(config));
uploadConfig(config);
};
const uploadConfig = (config) => {
API.saveReadConfig(config);
};
</script>
<style lang="scss" scoped>
:deep(.iconfont) {
font-family: iconfont;
font-style: normal;
}
:deep(.moon-icon) {
font-family: iconfont;
font-style: normal;
}
.settings-wrapper {
user-select: none;
margin: -13px;
// width: 478px;
// height: 350px;
text-align: left;
padding: 40px 0 40px 24px;
background: #ede7da url("../assets/imgs/themes/popup_1.png") repeat;
.settings-title {
font-size: 18px;
line-height: 22px;
margin-bottom: 28px;
font-family: FZZCYSK;
font-weight: 400;
}
.setting-list {
ul {
list-style: none outside none;
margin: 0;
padding: 0;
li {
list-style: none outside none;
i {
font: 12px / 16px PingFangSC-Regular, "-apple-system", Simsun;
display: inline-block;
min-width: 48px;
margin-right: 16px;
vertical-align: middle;
color: #666;
}
.theme-item {
line-height: 32px;
width: 34px;
height: 34px;
margin-right: 16px;
margin-top: 5px;
border-radius: 100%;
display: inline-block;
cursor: pointer;
text-align: center;
vertical-align: middle;
.iconfont {
display: none;
}
}
.selected {
color: #ed4259;
.iconfont {
display: inline;
}
}
}
.font-list,
.infinite-loading {
margin-top: 28px;
.font-item,
.infinite-loading-item {
width: 78px;
height: 34px;
cursor: pointer;
margin-right: 16px;
border-radius: 2px;
text-align: center;
vertical-align: middle;
display: inline-block;
font: 14px / 34px PingFangSC-Regular, HelveticaNeue-Light,
"Helvetica Neue Light", "Microsoft YaHei", sans-serif;
}
.font-item-input {
width: 168px;
color: #000000;
}
.selected {
color: #ed4259;
border: 1px solid #ed4259;
}
.font-item:hover,
.infinite-loading-item:hover {
border: 1px solid #ed4259;
color: #ed4259;
}
}
.font-size,
.read-width {
margin-top: 28px;
.resize {
display: inline-block;
width: 274px;
height: 34px;
vertical-align: middle;
border-radius: 2px;
span {
width: 89px;
height: 34px;
line-height: 34px;
display: inline-block;
cursor: pointer;
text-align: center;
vertical-align: middle;
em {
font-style: normal;
}
}
.less:hover,
.more:hover {
color: #ed4259;
}
.lang {
color: #a6a6a6;
font-weight: 400;
font-family: FZZCYSK;
}
b {
display: inline-block;
height: 20px;
vertical-align: middle;
}
}
}
}
}
}
.night {
:deep(.theme-item) {
border: 1px solid #666;
}
:deep(.selected) {
border: 1px solid #666;
}
:deep(.moon-icon) {
color: #ed4259;
}
:deep(.font-list),
.infinite-loading {
.font-item,
.infinite-loading-item {
border: 1px solid #666;
background: rgba(45, 45, 45, 0.5);
}
}
:deep(.resize) {
border: 1px solid #666;
background: rgba(45, 45, 45, 0.5);
b {
border-right: 1px solid #666;
}
}
}
.day {
:deep(.theme-item) {
border: 1px solid #e5e5e5;
}
:deep(.selected) {
border: 1px solid #ed4259;
}
:deep(.moon-icon) {
display: inline;
color: rgba(255, 255, 255, 0.2);
}
:deep(.font-list),
.infinite-loading {
.font-item,
.infinite-loading-item {
background: rgba(255, 255, 255, 0.5);
border: 1px solid rgba(0, 0, 0, 0.1);
}
}
:deep(.resize) {
border: 1px solid #e5e5e5;
background: rgba(255, 255, 255, 0.5);
b {
border-right: 1px solid #e5e5e5;
}
}
}
@media screen and (max-width: 500px) {
.settings-wrapper i {
display: flex !important;
flex-wrap: wrap;
padding-bottom: 5px !important;
}
}
</style>
@@ -0,0 +1,55 @@
<template>
<el-input
v-if="isBookSource"
id="debug-key"
v-model="searchKey"
placeholder="搜索书名、作者"
:prefix-icon="Search"
style="padding-bottom: 4px"
@keydown.enter="startDebug"
/>
<el-input
id="debug-text"
v-model="printDebug"
type="textarea"
readonly
rows="29"
placeholder="这里用于输出调试信息"
/>
</template>
<script setup>
import API from "@api";
import { Search } from "@element-plus/icons-vue";
const store = useSourceStore();
const printDebug = ref("");
const searchKey = ref("");
watchEffect(() => {
if (store.isDebuging) startDebug();
});
const appendDebugMsg = (msg) => {
let debugDom = document.querySelector("#debug-text");
debugDom.scrollTop = debugDom.scrollHeight;
printDebug.value += msg + "\n";
};
const startDebug = async () => {
printDebug.value = "";
await API.saveSource(store.currentSource);
API.debug(
store.currentSourceUrl,
searchKey.value || store.searchKey,
appendDebugMsg,
store.debugFinish
);
};
const isBookSource = computed(() => {
return /bookSource/.test(window.location.href);
});
</script>
<style lang="scss" scoped></style>
+57
View File
@@ -0,0 +1,57 @@
<script setup>
import { Link } from "@element-plus/icons-vue";
</script>
<template>
<el-link
:icon="Link"
href="https://alanskycn.gitee.io/teachme/"
target="_blank"
>书源制作教程</el-link
><br />
<el-link
:icon="Link"
href="https://zhuanlan.zhihu.com/p/29436838"
target="_blank"
>xpath基础教程</el-link
><br />
<el-link
:icon="Link"
href="https://zhuanlan.zhihu.com/p/32187820"
target="_blank"
>xpath高级教程</el-link
><br />
<el-link
:icon="Link"
href="https://www.w3cschool.cn/regex_rmjc"
target="_blank"
>正则表达式教程</el-link
><br />
<el-link :icon="Link" href="https://regexr-cn.com/" target="_blank"
>正则表达式在线验证工具</el-link
><br />
<div style="margin-top: 20px">
<span
><el-text
><code>^$()[]{}.?+*|</code> 这些是Java正则特殊符号,匹配需转义</el-text
></span
><br />
<span
><el-text><code>(?s)</code> 前缀表示跨行解析</el-text></span
><br />
<span
><el-text><code>(?m)</code> 前缀表示逐行匹配</el-text></span
><br />
<span
><el-text><code>(?i)</code> 前缀表示忽略大小写</el-text></span
><br />
</div>
</template>
<style lang="scss" scoped>
.el-link {
padding: 4px;
}
.el-text {
padding-top: 20px;
}
</style>
+40
View File
@@ -0,0 +1,40 @@
<template>
<el-input
v-model="sourceString"
type="textarea"
placeholder="这里输出序列化的JSON数据,可直接导入'阅读'APP"
rows="30"
@change="update"
style="margin-bottom: 4px"
></el-input>
</template>
<script setup>
import { useSourceStore } from "@/store";
const store = useSourceStore();
const sourceString = ref("");
const update = async (string) => {
try {
store.changeEditTabSource(JSON.parse(string));
} catch {
ElMessage({
message: "粘贴的源格式错误",
type: "error",
});
}
};
watchEffect(async () => {
let source = store.editTabSource;
if (Object.keys(source).length > 0) {
sourceString.value = JSON.stringify(source, null, 4);
} else {
sourceString.value = "";
}
});
</script>
<style>
.el-input {
width: 100%;
}
</style>
+148
View File
@@ -0,0 +1,148 @@
<template>
<el-input
v-model="searchKey"
class="search"
:prefix-icon="Search"
placeholder="筛选源"
/>
<div class="tool">
<el-button @click="importSourceFile" :icon="Folder"> 打开 </el-button>
<el-button
:disabled="sourceSelect.length === 0"
@click="outExport"
:icon="Download"
>
导出</el-button
>
<el-button
:icon="Delete"
@click="deleteSelectSources"
:disabled="sourceSelect.length === 0"
>删除</el-button
>
<el-button
type="danger"
:icon="Delete"
@click="clearAllSources"
:disabled="sources.length === 0"
>清空</el-button
>
</div>
<el-checkbox-group id="source-list" v-model="sourceSelect">
<el-checkbox
v-for="source in sourcesFiltered"
size="large"
border
:label="source"
:class="{ error: errorPushSources.includes(source) }"
@click="handleSourceClick(source)"
:key="source.bookSourceName"
>
{{ source.bookSourceName || source.sourceName }}
</el-checkbox>
</el-checkbox-group>
</template>
<script setup>
import { Folder, Delete, Download, Search } from "@element-plus/icons-vue";
import { isSourceContains } from "../utils/souce";
const store = useSourceStore();
const sourceSelect = ref([]);
const searchKey = ref("");
const { sources, errorPushSources } = storeToRefs(store);
const isBookSource = computed(() => {
return /bookSource/.test(window.location.href);
});
const handleSourceClick = (source) => {
store.changeCurrentSource(source);
};
const deleteSelectSources = () => {
store.deleteSources(sourceSelect.value);
sourceSelect.value = [];
};
const clearAllSources = () => {
store.clearAllSource();
sourceSelect.value = [];
};
//筛选源
const sourcesFiltered = computed(() => {
let key = searchKey.value;
if (key === "") return sources.value;
return (
sources.value
// @ts-ignore
.filter((source) => isSourceContains(source, key))
);
});
//导入本地文件
const importSourceFile = () => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json,.txt";
input.addEventListener("change", (e) => {
// @ts-ignore
const file = e.target.files[0];
var reader = new FileReader();
reader.readAsText(file);
reader.onload = () => {
try {
// @ts-ignore
const jsonData = JSON.parse(reader.result);
store.saveSources(jsonData);
} catch {
ElMessage({
message: "上传的源格式错误",
type: "error",
});
}
};
});
input.click();
};
const outExport = () => {
const exportFile = document.createElement("a");
let sources = store.sources,
sourceType = isBookSource.value ? "BookSource" : "RssSource";
exportFile.download = `${sourceType}_${Date()
.replace(/.*?\s(\d+)\s(\d+)\s(\d+:\d+:\d+).*/, "$2$1$3")
.replace(/:/g, "")}.json`;
let myBlob = new Blob([JSON.stringify(sources, null, 4)], {
type: "application/json",
});
exportFile.href = window.URL.createObjectURL(myBlob);
exportFile.click();
};
</script>
<style lang="scss" scoped>
.tool {
display: flex;
padding: 4px 0;
justify-content: space-between;
}
#source-list {
padding-top: 6px;
height: calc(100vh - 112px - 20px);
overflow-y: auto;
overflow-x: hidden;
:deep(.el-checkbox) {
margin-bottom: 4px;
width: 100%;
}
}
.error {
border-color: var(--el-color-error) !important;
color: var(--el-color-error) !important;
--el-checkbox-checked-text-color: var(--el-color-error);
--el-checkbox-checked-bg-color: var(--el-color-error);
--el-checkbox-checked-input-border-color: var(--el-color-error);
}
</style>
@@ -0,0 +1,75 @@
<template>
<el-tabs id="source-edit">
<el-tab-pane
v-for="{ name, children } in tabsData"
:label="name"
:key="name"
>
<el-form label-position="right" label-width="5em">
<el-form-item
v-for="{
type,
title,
namespace,
id,
array,
hint,
required,
} in children"
:label="title"
:key="title"
:required="required"
>
<el-input
v-if="type == 'String' && typeof namespace == 'undefined'"
type="textarea"
v-model="currentSource[id]"
:placeholder="hint"
autosize
/>
<el-input
v-if="type == 'String' && typeof namespace != 'undefined'"
type="textarea"
v-model="currentSource[namespace][id]"
:placeholder="hint"
autosize
/>
<el-switch v-if="type == 'Boolean'" v-model="currentSource[id]" />
<el-input-number
v-if="type == 'Number'"
v-model="currentSource[id]"
:min="0"
/>
<el-select v-if="type == 'Array'" v-model="currentSource[id]">
<el-option
v-for="(name, index) in array"
:value="index"
:key="name"
:label="name"
/>
</el-select>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
</template>
<script setup>
const store = useSourceStore();
const props = defineProps(["config"]);
const tabsData = Object.values(props.config);
const { currentSource } = storeToRefs(store);
</script>
<style lang="scss" scoped>
:deep(.el-tab-pane) {
height: calc(100vh - 40px);
overflow-y: auto;
}
</style>
@@ -0,0 +1,32 @@
<template>
<el-tabs v-model="current_tab">
<el-tab-pane
v-for="(tab, index) in tabData"
:key="tab[0]"
:name="tab[0]"
:label="tab[1]"
>
<source-json v-if="index == 0" />
<source-debug v-if="index == 1" />
<source-list v-if="index == 2" />
<source-help v-if="index == 3" />
</el-tab-pane>
</el-tabs>
</template>
<script setup>
import { useSourceStore } from "@/store";
const store = useSourceStore();
const { currentTab: current_tab } = storeToRefs(store);
const tabData = ref([
["editTab", "编辑源"],
["editDebug", "调试源"],
["editList", "源列表"],
["editHelp", "帮助信息"],
]);
</script>
<style lang="scss" scoped></style>
+316
View File
@@ -0,0 +1,316 @@
<template>
<div class="menu flex-column-center">
<el-button
v-for="button in buttons"
size="large"
:key="button.name"
@click="button.action"
>
{{ button.name }}
</el-button>
<el-button size="large" @click="() => (hotkeysDialogVisible = true)"
>快捷键</el-button
>
</div>
<el-dialog
v-model="hotkeysDialogVisible"
:show-close="false"
:before-close="stopRecordKeyDown"
>
<template #header="{ titleClass, titleId }">
<div class="hotkeys-header flex-space-between">
<div :id="titleId" :class="titleClass">
快捷键设置
<span v-if="recordKeyDowning">
<el-text> / 录入中 </el-text>
</span>
</div>
<el-button
:disabled="recordKeyDowning"
@click="bindHotKeys"
:icon="CircleCheckFilled"
>保存</el-button
>
</div>
</template>
<div class="hotkeys-settings flex-column-center">
<div
v-for="(button, index) in buttons"
:key="button.name"
class="hotkeys-item flex-space-between"
>
<span class="title"
><el-text>{{ button.name }}</el-text></span
>
<div class="hotkeys-item__content">
<div v-for="(key, index) in button.hotKeys" :key="key">
<kbd>{{ key }}</kbd>
<span v-if="index + 1 < button.hotKeys.length">
<el-text>+</el-text>
</span>
</div>
<span v-if="button.hotKeys.length == 0">未设置</span>
</div>
<el-button
:disabled="recordKeyDowning"
text
:icon="Edit"
@click="recordKeyDown(index)"
>编辑</el-button
>
</div>
</div>
</el-dialog>
</template>
<script setup>
import API from "@api";
import { CircleCheckFilled, Edit } from "@element-plus/icons-vue";
import hotkeys from "hotkeys-js";
import { isInvaildSource } from "../utils/souce";
const store = useSourceStore();
const pull = () => {
API.getSources().then(({ data }) => {
if (data.isSuccess) {
store.changeTabName("editList");
store.saveSources(data.data);
ElMessage({
message: `成功拉取${data.data.length}条源`,
type: "success",
});
} else {
ElMessage({
message: data.errorMsg ?? "后端错误",
type: "error",
});
}
});
};
const push = () => {
let sources = store.sources;
store.changeTabName("editList");
if (sources.length === 0) {
return ElMessage({
message: "空空如也",
type: "info",
});
}
ElMessage({
message: "正在推送中",
type: "info",
});
API.saveSources(sources).then(({ data }) => {
if (data.isSuccess) {
let okData = data.data;
if (Array.isArray(okData)) {
let failMsg = ``;
if (sources.length > okData.length) {
failMsg = "\n推送失败的源将用红色字体标注!";
store.setPushReturnSources(okData);
}
ElMessage({
message: `批量推送源到「阅读3.0APP」\n共计: ${
sources.length
}\n成功: ${okData.length}\n失败: ${
sources.length - okData.length
}${failMsg}`,
type: "success",
});
}
} else {
ElMessage({
message: `批量推送源失败!\nErrorMsg: ${data.errorMsg}`,
type: "error",
});
}
});
};
const conver2Tab = () => {
store.changeTabName("editTab");
store.changeEditTabSource(store.currentSource);
};
const conver2Source = () => {
store.changeCurrentSource(store.editTabSource);
};
const undo = () => {
store.editHistoryUndo();
};
const clearEdit = () => {
store.clearEdit();
ElMessage({
message: "已清除",
type: "success",
});
};
const redo = () => {
store.clearEdit();
store.clearAllHistory();
ElMessage({
message: "已清除所有历史记录",
type: "success",
});
};
const saveSource = () => {
let isBookSource = /bookSource/.test(location.href),
/** @type {import("@/source.js").Source} */
source = store.currentSource;
if (isInvaildSource(source)) {
API.saveSource(source).then(({ data }) => {
if (data.isSuccess) {
ElMessage({
message: `源《${
isBookSource ? source.bookSourceName : source.sourceName
}》已成功保存到「阅读3.0APP」`,
type: "success",
});
//save to store
store.saveCurrentSource();
} else {
ElMessage({
message: `源《${
isBookSource ? source.bookSourceName : source.sourceName
}》保存失败!\nErrorMsg: ${data.errorMsg}`,
type: "error",
});
}
});
} else {
ElMessage({
message: `请检查<必填>项是否全部填写`,
type: "error",
});
}
};
const debug = () => {
store.startDebug();
};
const buttons = ref(
Array.of(
{ name: "⇈推送源", hotKeys: [], action: push },
{ name: "⇊拉取源", hotKeys: [], action: pull },
{ name: "⋙生成源", hotKeys: [], action: conver2Tab },
{ name: "⋘编辑源", hotKeys: [], action: conver2Source },
{ name: "✗清空表单", hotKeys: [], action: clearEdit },
{ name: "↶撤销操作", hotKeys: [], action: undo },
{ name: "↷重做操作", hotKeys: [], action: redo },
{ name: "⇏调试源", hotKeys: [], action: debug },
{ name: "✓保存源", hotKeys: [], action: saveSource }
)
);
const hotkeysDialogVisible = ref(true);
const recordKeyDowning = ref(false);
const recordKeyDownIndex = ref(-1);
const stopRecordKeyDown = () => {
recordKeyDowning.value = false;
};
watch(hotkeysDialogVisible, (visibale) => {
if (!visibale) return hotkeys.unbind("*");
hotkeys.unbind();
/**监听按键 */
hotkeys("*", (event) => {
event.preventDefault();
if (recordKeyDowning.value && recordKeyDownIndex.value > -1)
buttons.value[recordKeyDownIndex.value].hotKeys =
// @ts-ignore
hotkeys.getPressedKeyString();
});
});
const recordKeyDown = (index) => {
recordKeyDowning.value = true;
ElMessage({
message: "按ESC键或者点击空白处结束录入",
type: "info",
});
buttons.value[index].hotKeys = [];
recordKeyDownIndex.value = index;
};
const bindHotKeys = () => {
hotkeysDialogVisible.value = false;
const hotKeysConfig = [];
buttons.value.forEach(({ hotKeys, action }) => {
hotkeys(hotKeys.join("+"), (event) => {
event.preventDefault();
action.call(null);
});
hotKeysConfig.push(hotKeys);
});
saveHotkeysConfig(hotKeysConfig);
};
const saveHotkeysConfig = (config) => {
localStorage.setItem("legado_web_hotkeys", JSON.stringify(config));
};
const readHotkeysConfig = () => {
try {
const config = JSON.parse(localStorage.getItem("legado_web_hotkeys"));
if (!Array.isArray(config) || config.length == 0) return;
buttons.value.forEach((button, index) => (button.hotKeys = config[index]));
hotkeysDialogVisible.value = false;
bindHotKeys();
} catch {
ElMessage({ message: "快捷键配置错误", type: "error" });
localStorage.removeItem("legado_web_hotkeys");
}
};
onMounted(() => {
/**读取热键配置 */
readHotkeysConfig();
});
</script>
<style lang="scss" scoped>
.flex-space-between {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.flex-column-center {
display: flex;
flex-direction: column;
justify-content: center;
}
.menu > .el-button {
margin: 4px;
padding: 1em;
width: 6em;
}
.hotkeys-item {
.title {
width: 5em;
display: flex;
justify-content: flex-end;
margin-right: 1em;
}
&__content {
display: flex;
flex-wrap: wrap;
flex: 1;
div {
margin-bottom: 1em;
}
span {
margin: 0.5em;
}
}
}
</style>