modules 添加web
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"></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"></em></span
|
||||
><b></b> <span class="lang">{{ fontSize }}</span
|
||||
><b></b>
|
||||
<span class="more" @click="moreFontSize"
|
||||
><em class="iconfont"></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"></em></span
|
||||
><b></b> <span class="lang">{{ readWidth }}</span
|
||||
><b></b>
|
||||
<span class="more" @click="moreReadWidth"
|
||||
><em class="iconfont"></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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user