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
+29 -20
View File
@@ -10,7 +10,7 @@
<div class="cover-img">
<img
class="cover"
:src="getCover(book.coverUrl)"
:src="getCover(book)"
:key="book.coverUrl"
@error.once="proxyImage"
alt=""
@@ -33,15 +33,17 @@
</div>
<div class="update-info" v-if="!isSearch">
<div class="dot"></div>
<div class="size">{{ book.totalChapterNum }}</div>
<div class="size">{{ (book as Book).totalChapterNum }}</div>
<div class="dot"></div>
<div class="date">{{ dateFormat(book.lastCheckTime) }}</div>
<div class="date">
{{ dateFormat((book as Book).lastCheckTime) }}
</div>
</div>
</div>
<div class="intro" v-if="isSearch">{{ book.intro }}</div>
<div class="dur-chapter" v-if="!isSearch">
已读{{ book.durChapterTitle }}
已读{{ (book as Book).durChapterTitle }}
</div>
<div class="last-chapter">最新{{ book.latestChapterTitle }}</div>
</div>
@@ -49,26 +51,32 @@
</div>
</div>
</template>
<script setup>
import { dateFormat, isLegadoUrl } from "../utils/utils";
import API from "@api";
<script setup lang="ts">
import type { Book, SeachBook } from '@/book'
import { dateFormat, isLegadoUrl } from '../utils/utils'
import API from '@api'
const props = defineProps<{
books: Array<Book | SeachBook>
isSearch: boolean
}>()
const props = defineProps(["books", "isSearch"]);
const emit = defineEmits(["bookClick"]);
const handleClick = (book) => emit("bookClick", book);
const getCover = (coverUrl) => {
return isLegadoUrl(coverUrl) ? API.getProxyCoverUrl(coverUrl) : coverUrl;
};
const proxyImage = (event) => {
event.target.src = API.getProxyCoverUrl(event.target.src);
};
const emit = defineEmits(['bookClick'])
const handleClick = (book: Book | SeachBook) => emit('bookClick', book)
const getCover = ({ bookUrl, coverUrl }: Book | SeachBook) => {
if (coverUrl === undefined) return API.getProxyCoverUrl(bookUrl)
return isLegadoUrl(coverUrl) ? API.getProxyCoverUrl(coverUrl) : coverUrl
}
const proxyImage = (evt: Event) => {
const target = evt.target as HTMLImageElement
target.src = API.getProxyCoverUrl(target.src)
}
const subJustify = computed(() =>
props.isSearch ? "space-between" : "flex-start",
);
props.isSearch ? 'space-between' : 'flex-start',
)
</script>
<style lang="scss" scoped>
<style scoped>
.books-wrapper {
overflow: auto;
@@ -119,7 +127,7 @@ const subJustify = computed(() =>
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: v-bind("subJustify");
justify-content: v-bind('subJustify');
font-size: 12px;
font-weight: 600;
color: #6b6b6b;
@@ -149,6 +157,7 @@ const subJustify = computed(() =>
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
line-clamp: 1;
text-align: left;
}
}
+17 -13
View File
@@ -11,25 +11,29 @@
</div>
</div>
</template>
<script setup>
const props = defineProps([
"index",
"source",
"gotoChapter",
"currentChapterIndex",
]);
<script setup lang="ts">
import type { BookChapter } from '@/book'
const isSelected = (idx) => {
return idx == props.currentChapterIndex;
};
const props = defineProps<{
index: number
source: BookChapter | { index: number; catas: BookChapter[] }
gotoChapter: (chapter: BookChapter) => void
currentChapterIndex: number
}>()
const isSelected = (idx: number) => {
return idx == props.currentChapterIndex
}
// PC端 一个虚拟列表中有两个章节
const catas = computed(() => {
return props.source?.catas ?? [props.source];
});
const source = props.source
if ('catas' in source) return source.catas
return [props.source as BookChapter]
})
</script>
<style lang="scss" scoped>
<style scoped>
.selected {
color: #eb4259;
}
+80 -73
View File
@@ -17,116 +17,123 @@
</div>
</template>
<script setup>
import { isLegadoUrl } from "@/utils/utils";
import API from "@api";
import jump from "@/plugins/jump";
<script setup lang="ts">
import { isLegadoUrl } from '@/utils/utils'
import API from '@api'
import jump from '@/plugins/jump'
import type { webReadConfig } from '@/web'
const props = defineProps({
chapterIndex: { type: Number, required: true },
contents: { type: Array, required: true },
title: { type: String, required: true },
spacing: { type: Object, required: true },
fontFamily: { type: String, required: true },
fontSize: { type: String, required: true },
});
const store = useBookStore()
const readWidth = computed(() => store.config.readWidth)
const bookUrl = computed(() => store.readingBook.bookUrl)
const props = defineProps<{
chapterIndex: number
contents: Array<string>
title: string
spacing: webReadConfig['spacing']
fontFamily: string
fontSize: string
}>()
const getImageSrc = (content) => {
const imgPattern = /<img[^>]*src="([^"]*(?:"[^>]+\})?)"[^>]*>/;
const src = content.match(imgPattern)[1];
const getImageSrc = (content: string) => {
const imgPattern = /<img[^>]*src="([^"]*(?:"[^>]+\})?)"[^>]*>/
const src = content.match(imgPattern)![1] //reg tested in template
if (isLegadoUrl(src))
return API.getProxyImageUrl(src, useBookStore().config.readWidth);
return src;
};
const proxyImage = (event) => {
event.target.src = API.getProxyImageUrl(
event.target.src,
useBookStore().config.readWidth,
);
};
return API.getProxyImageUrl(
bookUrl.value,
src,
useBookStore().config.readWidth,
)
return src
}
const proxyImage = (event: Event) => {
;(event.target as HTMLImageElement).src = API.getProxyImageUrl(
bookUrl.value,
(event.target as HTMLImageElement).src,
readWidth.value,
)
}
const calculateWordCount = (paragraph) => {
const imgPattern = /<img[^>]*src="[^"]*(?:"[^>]+\})?"[^>]*>/g;
const calculateWordCount = (paragraph: string) => {
const imgPattern = /<img[^>]*src="[^"]*(?:"[^>]+\})?"[^>]*>/g
//内嵌图片文字为1
const imagePlaceHolder = " ";
return paragraph.replaceAll(imgPattern, imagePlaceHolder).length;
};
const imagePlaceHolder = ' '
return paragraph.replaceAll(imgPattern, imagePlaceHolder).length
}
const chapterPos = computed(() => {
let pos = -1;
return Array.from(props.contents, (content) => {
pos += calculateWordCount(content) + 1; //计算上一段的换行符
return pos;
});
});
let pos = -1
return Array.from(props.contents, content => {
pos += calculateWordCount(content) + 1 //计算上一段的换行符
return pos
})
})
const titleRef = ref();
const paragraphRef = ref();
const scrollToReadedLength = (length) => {
if (length === 0) return;
let paragraphIndex = chapterPos.value.findIndex(
(wordCount) => wordCount >= length,
);
if (paragraphIndex === -1) return;
const titleRef = ref<HTMLElement>()
const paragraphRef = ref<HTMLParagraphElement[]>()
const scrollToReadedLength = (length: number) => {
if (length === 0) return
const paragraphIndex = chapterPos.value.findIndex(
wordCount => wordCount >= length,
)
if (paragraphIndex === -1) return
nextTick(() => {
jump(paragraphRef.value[paragraphIndex], {
jump(paragraphRef.value![paragraphIndex], {
duration: 0,
});
});
};
})
})
}
defineExpose({
scrollToReadedLength,
});
let intersectionObserver = null;
const emit = defineEmits(["readedLengthChange"]);
})
let intersectionObserver: IntersectionObserver | null = null
const emit = defineEmits(['readedLengthChange'])
onMounted(() => {
intersectionObserver = new IntersectionObserver(
(entries) => {
for (let { target, isIntersecting } of entries) {
entries => {
for (const { target, isIntersecting } of entries) {
if (isIntersecting) {
emit(
"readedLengthChange",
'readedLengthChange',
props.chapterIndex,
// @ts-ignore
parseInt(target.dataset.chapterpos),
);
parseInt((target as HTMLElement).dataset.chapterpos as string),
)
}
}
},
{
rootMargin: `0px 0px -${window.innerHeight - 24}px 0px`,
},
);
intersectionObserver.observe(titleRef.value);
paragraphRef.value.forEach((element) => {
intersectionObserver.observe(element);
});
});
)
intersectionObserver.observe(titleRef.value!)
paragraphRef.value!.forEach(element => {
intersectionObserver!.observe(element)
})
})
onUnmounted(() => {
intersectionObserver?.disconnect();
intersectionObserver = null;
});
intersectionObserver?.disconnect()
intersectionObserver = null
})
</script>
<style lang="scss" scoped>
<style scoped>
.title {
margin-bottom: 57px;
font:
24px / 32px PingFangSC-Regular,
HelveticaNeue-Light,
"Helvetica Neue Light",
"Microsoft YaHei",
'Helvetica Neue Light',
'Microsoft YaHei',
sans-serif;
}
p {
display: block;
word-wrap: break-word;
// word-break: break-all;
letter-spacing: calc(v-bind("props.spacing.letter") * 1em);
line-height: calc(1 + v-bind("props.spacing.line"));
margin: calc(v-bind("props.spacing.paragraph") * 1em) 0;
/* word-break: break-all; */
letter-spacing: calc(v-bind('props.spacing.letter') * 1em);
line-height: calc(1 + v-bind('props.spacing.line'));
margin: calc(v-bind('props.spacing.paragraph') * 1em) 0;
:deep(img) {
height: 1em;
+49 -45
View File
@@ -19,81 +19,85 @@
</div>
</template>
<script setup>
import VirtualList from "vue3-virtual-scroll-list";
import settings from "../config/themeConfig";
import "../assets/fonts/popfont.css";
import CatalogItem from "./CatalogItem.vue";
<script setup lang="ts">
import VirtualList from 'vue3-virtual-scroll-list'
import settings from '../config/themeConfig'
import '../assets/fonts/popfont.css'
import CatalogItem from './CatalogItem.vue'
import type { BookChapter } from '@/book'
const store = useBookStore();
const store = useBookStore()
const { catalog, popCataVisible, miniInterface } = storeToRefs(store);
const { catalog, popCataVisible, miniInterface } = storeToRefs(store)
//主题
const isNight = computed(() => store.theme);
const theme = computed(() => store.theme);
const isNight = computed(() => store.theme)
const theme = computed(() => store.theme)
const popupTheme = computed(() => {
return {
background: settings.themes[theme.value].popup,
};
});
}
})
//虚拟列表 数据源
const virtualListdata = computed(() => {
let catalogValue = catalog.value;
if (miniInterface.value) return catalogValue;
const catalogValue = catalog.value
if (miniInterface.value) return catalogValue
// pc端 virtualListIitem有2个章节
let length = Math.ceil(catalogValue.length / 2);
let virtualListDataSource = new Array(length);
const length = Math.ceil(catalogValue.length / 2)
const virtualListDataSource = new Array<{
index: number
catas: BookChapter[]
}>(length)
let i = 0;
let i = 0
while (i < length) {
virtualListDataSource[i] = {
index: i,
catas: catalogValue.slice(2 * i, 2 * i + 2),
};
i++;
}
i++
}
return virtualListDataSource;
});
return virtualListDataSource
})
//打开目录 计算当前章节对应的虚拟列表位置
const virtualListRef = ref();
const virtualListRef = ref()
const currentChapterIndex = computed({
get: () => store.readingBook.index,
set: (value) => (store.readingBook.index = value),
});
get: () => store.readingBook.chapterIndex,
set: value => (store.readingBook.chapterIndex = value),
})
const virtualListIndex = computed(() => {
let index = currentChapterIndex.value;
if (miniInterface.value) return index;
const index = currentChapterIndex.value
if (miniInterface.value) return index
// pc端 virtualListIitem有2个章节
return Math.floor(index / 2);
});
return Math.floor(index / 2)
})
onUpdated(() => {
// dom更新触发ResizeObserver,更新虚拟列表内部的sizes Map
if (!popCataVisible.value) return;
virtualListRef.value.scrollToIndex(virtualListIndex.value);
});
if (!popCataVisible.value) return
virtualListRef.value.scrollToIndex(virtualListIndex.value)
})
// 点击加载对应章节内容
const emit = defineEmits(["getContent"]);
const gotoChapter = (note) => {
const chapterIndex = catalog.value.indexOf(note);
currentChapterIndex.value = chapterIndex;
store.setPopCataVisible(false);
store.setContentLoading(true);
store.saveBookProgress();
emit("getContent", chapterIndex);
};
const emit = defineEmits(['getContent'])
const gotoChapter = (chapter: BookChapter) => {
const chapterIndex = catalog.value.indexOf(chapter)
currentChapterIndex.value = chapterIndex
store.setPopCataVisible(false)
store.setContentLoading(true)
store.saveBookProgress()
emit('getContent', chapterIndex)
}
</script>
<style lang="scss" scoped>
<style scoped>
.cata-wrapper {
margin: -16px;
padding: 18px 0 24px 25px;
// background: #ede7da url('../assets/imgs/themes/popup_1.png') repeat;
/* background: #ede7da url('../assets/imgs/themes/popup_1.png') repeat; */
.title {
font-size: 18px;
font-weight: 400;
@@ -105,14 +109,14 @@ const gotoChapter = (note) => {
}
:deep(.data-wrapper) {
.cata {
//width: 50%;
/*width: 50%;*/
height: 40px;
cursor: pointer;
font:
16px / 40px PingFangSC-Regular,
HelveticaNeue-Light,
"Helvetica Neue Light",
"Microsoft YaHei",
'Helvetica Neue Light',
'Microsoft YaHei',
sans-serif;
}
}
+116 -130
View File
@@ -62,19 +62,10 @@
<el-button
type="primary"
size="small"
@click="
setCustomFont();
customFontSavePopVisible = false;
"
@click="setCustomFont(), (customFontSavePopVisible = false)"
>确定</el-button
>
<el-button
type="primary"
size="small"
@click="
loadFontFromURL();
customFontSavePopVisible = false;
"
<el-button type="primary" size="small" @click="loadFontFromURL()"
>网络下载</el-button
>
</div>
@@ -183,197 +174,192 @@
</div>
</template>
<script setup>
import "../assets/fonts/popfont.css";
import "../assets/fonts/iconfont.css";
import settings from "../config/themeConfig";
import API from "@api";
const store = useBookStore();
<script setup lang="ts">
import '../assets/fonts/popfont.css'
import '../assets/fonts/iconfont.css'
import settings from '../config/themeConfig'
import API from '@api'
import { useDebounceFn } from '@vueuse/shared'
const store = useBookStore()
const saveConfigDebounce = useDebounceFn(
() => API.saveReadConfig(store.config),
500,
)
//阅读界面设置改变时保存同步配置
let configChanged = false;
watch(
() => store.config,
(newValue) => {
localStorage.setItem("config", JSON.stringify(newValue));
configChanged = true;
() => {
saveConfigDebounce()
},
{
deep: 2, //深度为2
},
);
// 设置页面关闭时同步设置到阅读APP
watch(
() => store.readSettingsVisible,
(visbile) => {
if (!visbile && configChanged)
API.saveReadConfig(store.config).then(() => (configChanged = false));
},
);
)
//主题颜色
const theme = computed(() => store.theme);
const isNight = computed(() => store.isNight);
const moonIcon = computed(() => (theme.value == 6 ? "" : ""));
const theme = computed(() => store.theme)
const isNight = computed(() => store.isNight)
const moonIcon = computed(() => (theme.value == 6 ? '' : ''))
const themeColors = [
{
background: "rgba(250, 245, 235, 0.8)",
background: 'rgba(250, 245, 235, 0.8)',
},
{
background: "rgba(245, 234, 204, 0.8)",
background: 'rgba(245, 234, 204, 0.8)',
},
{
background: "rgba(230, 242, 230, 0.8)",
background: 'rgba(230, 242, 230, 0.8)',
},
{
background: "rgba(228, 241, 245, 0.8)",
background: 'rgba(228, 241, 245, 0.8)',
},
{
background: "rgba(245, 228, 228, 0.8)",
background: 'rgba(245, 228, 228, 0.8)',
},
{
background: "rgba(224, 224, 224, 0.8)",
background: 'rgba(224, 224, 224, 0.8)',
},
{
background: "rgba(0, 0, 0, 0.5)",
background: 'rgba(0, 0, 0, 0.5)',
},
];
]
const popupTheme = computed(() => {
return {
background: settings.themes[theme.value].popup,
};
});
const setTheme = (theme) => {
store.config.theme = theme;
};
}
})
const setTheme = (theme: number) => {
store.config.theme = theme
}
//预置字体
const fonts = ref(["雅黑", "宋体", "楷书"]);
const setFont = (font) => {
store.config.font = font;
};
const fonts = ref(['雅黑', '宋体', '楷书'])
const setFont = (font: number) => {
store.config.font = font
}
const selectedFont = computed(() => {
return store.config.font;
});
return store.config.font
})
//自定义字体
const customFontName = ref(store.config.customFontName);
const customFontSavePopVisible = ref(false);
const customFontName = ref(store.config.customFontName)
const customFontSavePopVisible = ref(false)
const setCustomFont = () => {
store.config.font = -1;
store.config.customFontName = customFontName.value;
};
customFontSavePopVisible.value = false
store.config.font = -1
store.config.customFontName = customFontName.value
}
// 加载网络字体
const loadFontFromURL = () => {
ElMessageBox.prompt("请输入 字体网络链接", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
customFontSavePopVisible.value = false
ElMessageBox.prompt('请输入 字体网络链接', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^https?:.+$/,
inputErrorMessage: "url 形式不正确",
inputErrorMessage: 'url 形式不正确',
beforeClose: (action, instance, done) => {
if (action === "confirm") {
instance.confirmButtonLoading = true;
instance.confirmButtonText = "下载中……";
if (action === 'confirm') {
instance.confirmButtonLoading = true
instance.confirmButtonText = '下载中……'
// instance.inputValue
const url = instance.inputValue;
if (typeof FontFace !== "function") {
ElMessage.error("浏览器不支持FontFace");
return done();
const url = instance.inputValue
if (typeof FontFace !== 'function') {
ElMessage.error('浏览器不支持FontFace')
return done()
}
const fontface = new FontFace(customFontName.value, `url("${url}")`);
//@ts-ignore
document.fonts.add(fontface);
const fontface = new FontFace(customFontName.value, `url("${url}")`)
document.fonts.add(fontface)
fontface
.load()
//API.getBookShelf()
.then(function () {
instance.confirmButtonLoading = false;
ElMessage.info("字体加载成功!");
setCustomFont();
done();
instance.confirmButtonLoading = false
ElMessage.info('字体加载成功!')
setCustomFont()
done()
})
.catch(function (error) {
instance.confirmButtonLoading = false;
instance.confirmButtonText = "确定";
ElMessage.error("下载失败,请检查您输入的 url");
throw error;
});
instance.confirmButtonLoading = false
instance.confirmButtonText = '确定'
ElMessage.error('下载失败,请检查您输入的 url')
throw error
})
} else {
done();
done()
}
},
});
};
})
}
//字体大小
const fontSize = computed(() => {
return store.config.fontSize;
});
return store.config.fontSize
})
const moreFontSize = () => {
if (store.config.fontSize < 48) store.config.fontSize += 2;
};
if (store.config.fontSize < 48) store.config.fontSize += 2
}
const lessFontSize = () => {
if (store.config.fontSize > 12) store.config.fontSize -= 2;
};
if (store.config.fontSize > 12) store.config.fontSize -= 2
}
//字 行 段落间距
const spacing = computed(() => {
return store.config.spacing;
});
return store.config.spacing
})
const lessLetterSpacing = () => {
store.config.spacing.letter -= 0.01;
};
store.config.spacing.letter -= 0.01
}
const moreLetterSpacing = () => {
store.config.spacing.letter += 0.01;
};
store.config.spacing.letter += 0.01
}
const lessLineSpacing = () => {
store.config.spacing.line -= 0.1;
};
store.config.spacing.line -= 0.1
}
const moreLineSpacing = () => {
store.config.spacing.line += 0.1;
};
store.config.spacing.line += 0.1
}
const lessParagraphSpacing = () => {
store.config.spacing.paragraph -= 0.1;
};
store.config.spacing.paragraph -= 0.1
}
const moreParagraphSpacing = () => {
store.config.spacing.paragraph += 0.1;
};
store.config.spacing.paragraph += 0.1
}
//页面宽度
const readWidth = computed(() => {
return store.config.readWidth;
});
return store.config.readWidth
})
const moreReadWidth = () => {
// 此时会截断页面
if (store.config.readWidth + 160 + 2 * 68 > window.innerWidth) return;
store.config.readWidth += 160;
};
if (store.config.readWidth + 160 + 2 * 68 > window.innerWidth) return
store.config.readWidth += 160
}
const lessReadWidth = () => {
if (store.config.readWidth > 640) store.config.readWidth -= 160;
};
if (store.config.readWidth > 640) store.config.readWidth -= 160
}
//翻页速度
const jumpDuration = computed(() => {
return store.config.jumpDuration;
});
return store.config.jumpDuration
})
const moreJumpDuration = () => {
store.config.jumpDuration += 100;
};
store.config.jumpDuration += 100
}
const lessJumpDuration = () => {
if (store.config.jumpDuration === 0) return;
store.config.jumpDuration -= 100;
};
if (store.config.jumpDuration === 0) return
store.config.jumpDuration -= 100
}
//无限加载
const infiniteLoading = computed(() => {
return store.config.infiniteLoading;
});
const setInfiniteLoading = (loading) => {
store.config.infiniteLoading = loading;
};
return store.config.infiniteLoading
})
const setInfiniteLoading = (loading: boolean) => {
store.config.infiniteLoading = loading
}
</script>
<style lang="scss" scoped>
<style scoped>
:deep(.iconfont) {
font-family: iconfont;
font-style: normal;
@@ -387,11 +373,11 @@ const setInfiniteLoading = (loading) => {
.settings-wrapper {
user-select: none;
margin: -13px;
// width: 478px;
// height: 350px;
/* width: 478px;
height: 350px; */
text-align: left;
padding: 40px 0 40px 24px;
background: #ede7da url("../assets/imgs/themes/popup_1.png") repeat;
background: #ede7da url('../assets/imgs/themes/popup_1.png') repeat;
.settings-title {
font-size: 18px;
@@ -416,7 +402,7 @@ const setInfiniteLoading = (loading) => {
i {
font:
12px / 16px PingFangSC-Regular,
"-apple-system",
'-apple-system',
Simsun;
display: inline-block;
min-width: 48px;
@@ -468,8 +454,8 @@ const setInfiniteLoading = (loading) => {
font:
14px / 34px PingFangSC-Regular,
HelveticaNeue-Light,
"Helvetica Neue Light",
"Microsoft YaHei",
'Helvetica Neue Light',
'Microsoft YaHei',
sans-serif;
}
.font-item-input {
+22 -22
View File
@@ -18,49 +18,49 @@
/>
</template>
<script setup>
import API from "@api";
import { Search } from "@element-plus/icons-vue";
<script setup lang="ts">
import API from '@api'
import { Search } from '@element-plus/icons-vue'
const store = useSourceStore();
const store = useSourceStore()
const printDebug = ref("");
const searchKey = ref("");
const printDebug = ref('')
const searchKey = ref('')
watch(
() => store.isDebuging,
() => {
if (store.isDebuging) startDebug();
if (store.isDebuging) startDebug()
},
);
)
const appendDebugMsg = (msg) => {
let debugDom = document.querySelector("#debug-text");
debugDom.scrollTop = debugDom.scrollHeight;
printDebug.value += msg + "\n";
};
const appendDebugMsg = (msg: string) => {
const debugDom = document.querySelector('#debug-text')
debugDom!.scrollTop = debugDom!.scrollHeight
printDebug.value += msg + '\n'
}
const startDebug = async () => {
printDebug.value = "";
printDebug.value = ''
try {
await API.saveSource(store.currentSource);
await API.saveSource(store.currentSource)
} catch (e) {
store.debugFinish();
throw e;
store.debugFinish()
throw e
}
API.debug(
store.currentSourceUrl,
searchKey.value || store.searchKey,
appendDebugMsg,
store.debugFinish,
);
};
)
}
const isBookSource = computed(() => {
return /bookSource/i.test(window.location.href);
});
return /bookSource/i.test(window.location.href)
})
</script>
<style lang="scss" scoped>
<style scoped>
:deep(#debug-text) {
height: calc(100vh - 45px - 36px - 5px);
}
+3 -3
View File
@@ -1,5 +1,5 @@
<script setup>
import { Link } from "@element-plus/icons-vue";
<script setup lang="ts">
import { Link } from '@element-plus/icons-vue'
</script>
<template>
<el-link :icon="Link" href="/help/#appHelp" target="_blank"
@@ -53,7 +53,7 @@ import { Link } from "@element-plus/icons-vue";
</div>
</template>
<style lang="scss" scoped>
<style scoped>
.el-link {
padding: 4px;
}
+19 -16
View File
@@ -8,32 +8,35 @@
edit: sourceUrl == currentSourceUrl,
}"
>
{{ source.bookSourceName || source.sourceName }}
{{ getSourceName(source) }}
<el-button text :icon="Edit" @click="handleSourceClick(source)" />
</el-checkbox>
</template>
<script setup>
import { Edit } from "@element-plus/icons-vue";
import { getSourceUniqueKey } from "@/utils/souce";
<script setup lang="ts">
import { Edit } from '@element-plus/icons-vue'
import { getSourceUniqueKey, getSourceName } from '@/utils/souce'
import type { Source } from '@/source'
const props = defineProps(["source"]);
const props = defineProps<{
source: Source
}>()
const store = useSourceStore();
const store = useSourceStore()
const currentSourceUrl = computed(() => store.currentSourceUrl);
const sourceUrl = computed(() => getSourceUniqueKey(props.source));
const currentSourceUrl = computed(() => store.currentSourceUrl)
const sourceUrl = computed(() => getSourceUniqueKey(props.source))
const handleSourceClick = (source) => {
store.changeCurrentSource(source);
};
const handleSourceClick = (source: Source) => {
store.changeCurrentSource(source)
}
const isSaveError = computed(() => {
const map = store.savedSourcesMap;
if (map.size == 0) return false;
return !map.has(sourceUrl.value);
});
const map = store.savedSourcesMap
if (map.size == 0) return false
return !map.has(sourceUrl.value)
})
</script>
<style lang="scss" scoped>
<style scoped>
:deep(.el-checkbox__label) {
flex: 1;
display: flex;
+14 -14
View File
@@ -9,30 +9,30 @@
style="margin-bottom: 4px"
></el-input>
</template>
<script setup>
import { useSourceStore } from "@/store";
<script setup lang="ts">
import { useSourceStore } from '@/store'
const store = useSourceStore();
const sourceString = ref("");
const update = async (string) => {
const store = useSourceStore()
const sourceString = ref('')
const update = async (string: string) => {
try {
store.changeEditTabSource(JSON.parse(string));
store.changeEditTabSource(JSON.parse(string))
} catch {
ElMessage({
message: "粘贴的源格式错误",
type: "error",
});
message: '粘贴的源格式错误',
type: 'error',
})
}
};
}
watchEffect(async () => {
let source = store.editTabSource;
const source = store.editTabSource
if (Object.keys(source).length > 0) {
sourceString.value = JSON.stringify(source, null, 4);
sourceString.value = JSON.stringify(source, null, 4)
} else {
sourceString.value = "";
sourceString.value = ''
}
});
})
</script>
<style scoped>
:deep(.el-input) {
+75 -81
View File
@@ -32,7 +32,7 @@
<el-checkbox-group id="source-list" v-model="sourceUrlSelect">
<virtual-list
style="height: 100%; overflow-y: auto; overflow-x: hidden"
:data-key="(source) => source.bookSourceUrl || source.sourceUrl"
:data-key="(source: Source) => getSourceName(source)"
:data-sources="sourcesFiltered"
:data-component="SourceItem"
:estimate-size="45"
@@ -40,116 +40,110 @@
</el-checkbox-group>
</template>
<script setup>
import API from "@api";
import { Folder, Delete, Download, Search } from "@element-plus/icons-vue";
<script setup lang="ts">
import API from '@api'
import { Folder, Delete, Download, Search } from '@element-plus/icons-vue'
import {
isSourceMatches,
getSourceUniqueKey,
getSourceName,
convertSourcesToMap,
} from "@utils/souce";
import VirtualList from "vue3-virtual-scroll-list";
import SourceItem from "./SourceItem.vue";
} from '@utils/souce'
import VirtualList from 'vue3-virtual-scroll-list'
import SourceItem from './SourceItem.vue'
import type { Source } from '@/source'
const store = useSourceStore();
const sourceUrlSelect = ref([]);
const searchKey = ref("");
const sources = computed(() => store.sources);
const store = useSourceStore()
const sourceUrlSelect = ref<string[]>([])
const searchKey = ref('')
const sources = computed(() => store.sources)
// 筛选源
/** @type Ref<import('@/source').Source[]> */
const sourcesFiltered = computed(() => {
const key = searchKey.value;
if (key === "") return sources.value;
return (
sources.value
// @ts-ignore
.filter((source) => isSourceMatches(source, key))
);
});
/* 筛选源 */
const sourcesFiltered = computed<Source[]>(() => {
const key = searchKey.value
if (key === '') return sources.value
return sources.value.filter(source => isSourceMatches(source, key))
})
// 计算当前筛选关键词下的选中源
/** @type Ref<import('@/source').Source[]> */
const sourceSelect = computed(() => {
const urls = sourceUrlSelect.value;
if (urls.length == 0) return [];
const sourceSelect = computed<Source[]>(() => {
const urls = sourceUrlSelect.value
if (urls.length == 0) return []
const sourcesFilteredMap =
searchKey.value == ""
searchKey.value == ''
? store.sourcesMap
: convertSourcesToMap(sourcesFiltered.value);
: convertSourcesToMap(sourcesFiltered.value)
return urls.reduce((sources, sourceUrl) => {
const source = sourcesFilteredMap.get(sourceUrl);
if (source) sources.push(source);
return sources;
}, []);
});
const source = sourcesFilteredMap.get(sourceUrl)
if (source) sources.push(source)
return sources
}, [] as Source[])
})
const deleteSelectSources = () => {
const sourceSelectValue = sourceSelect.value;
const sourceSelectValue = sourceSelect.value
API.deleteSource(sourceSelectValue).then(({ data }) => {
if (!data.isSuccess) return ElMessage.error(data.errorMsg);
store.deleteSources(sourceSelectValue);
const sourceUrlSelectRawValue = toRaw(sourceUrlSelect.value);
sourceSelectValue.forEach((source) => {
const index = sourceUrlSelectRawValue.indexOf(getSourceUniqueKey(source));
if (index > -1) sourceUrlSelectRawValue.splice(index, 1);
});
sourceUrlSelect.value = sourceUrlSelectRawValue;
});
};
if (!data.isSuccess) return ElMessage.error(data.errorMsg)
store.deleteSources(sourceSelectValue)
const sourceUrlSelectRawValue = toRaw(sourceUrlSelect.value)
sourceSelectValue.forEach(source => {
const index = sourceUrlSelectRawValue.indexOf(getSourceUniqueKey(source))
if (index > -1) sourceUrlSelectRawValue.splice(index, 1)
})
sourceUrlSelect.value = sourceUrlSelectRawValue
})
}
const clearAllSources = () => {
store.clearAllSource();
sourceUrlSelect.value = [];
};
store.clearAllSource()
sourceUrlSelect.value = []
}
//导入本地文件
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];
const reader = new FileReader();
reader.readAsText(file);
const input = document.createElement('input')
input.type = 'file'
input.accept = '.json,.txt'
input.addEventListener('change', () => {
const files = input.files
if (files === null) {
return ElMessage.info('未选择文件')
}
const reader = new FileReader()
reader.readAsText(files[0])
reader.onload = () => {
try {
// @ts-ignore
const jsonData = JSON.parse(reader.result);
store.saveSources(jsonData);
} catch {
ElMessage({
message: "上传的源格式错误",
type: "error",
});
const jsonData = JSON.parse(reader.result as string)
store.saveSources(jsonData)
} catch (e: unknown) {
ElMessage.error('上传的源格式错误: ' + (e as Error).message)
}
};
});
input.click();
};
}
})
input.click()
}
const isBookSource = /bookSource/i.test(window.location.href);
const isBookSource = /bookSource/i.test(window.location.href)
const outExport = () => {
const exportFile = document.createElement("a");
let sources =
const exportFile = document.createElement('a')
const sources =
sourceUrlSelect.value.length === 0
? sourcesFiltered.value
: sourceSelect.value,
sourceType = isBookSource ? "BookSource" : "RssSource";
sourceType = isBookSource ? 'BookSource' : 'RssSource'
exportFile.download = `${sourceType}_${Date()
.replace(/.*?\s(\d+)\s(\d+)\s(\d+:\d+:\d+).*/, "$2$1$3")
.replace(/:/g, "")}.json`;
.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();
window.URL.revokeObjectURL(exportFile.href); //avoid memory leak
};
const myBlob = new Blob([JSON.stringify(sources, null, 4)], {
type: 'application/json',
})
exportFile.href = window.URL.createObjectURL(myBlob)
exportFile.click()
window.URL.revokeObjectURL(exportFile.href) //avoid memory leak
}
</script>
<style lang="scss" scoped>
<style scoped>
.tool {
display: flex;
margin: 4px 0;
+19 -11
View File
@@ -14,7 +14,7 @@
id,
array,
hint,
required,
required = false,
} in children"
:label="title"
:key="title"
@@ -35,20 +35,26 @@
autosize
/>
<el-switch v-if="type == 'Boolean'" v-model="currentSource[id]" />
<el-switch
v-if="(type as string) === 'Boolean'"
v-model="currentSource[id]"
/>
<el-input-number
v-if="type == 'Number'"
v-if="(type as string) === 'Number'"
v-model="currentSource[id]"
:min="0"
/>
<el-select v-if="type == 'Array'" v-model="currentSource[id]">
<el-select
v-if="(type as string) === 'Array'"
v-model="currentSource[id]"
>
<el-option
v-for="(name, index) in array"
v-for="(optionName, index) in array"
:value="index"
:key="name"
:label="name"
:key="optionName"
:label="optionName"
/>
</el-select>
</el-form-item>
@@ -57,11 +63,13 @@
</el-tabs>
</template>
<script setup>
const store = useSourceStore();
defineProps(["config"]);
<script setup lang="ts">
import type { SourceConfig } from '@/config/sourceConfig'
const currentSource = computed(() => store.currentSource);
const store = useSourceStore()
defineProps<{ config: SourceConfig }>()
const currentSource = computed(() => store.currentSource)
/*
修改currentSource的属性 没有直接修改本身
const { currentSource } = storeToRefs(store);
+10 -10
View File
@@ -14,22 +14,22 @@
</el-tabs>
</template>
<script setup>
import { useSourceStore } from "@/store";
<script setup lang="ts">
import { useSourceStore } from '@/store'
const store = useSourceStore();
const store = useSourceStore()
const current_tab = computed({
get: () => store.currentTab,
set: (val) => (store.currentTab = val),
});
set: val => (store.currentTab = val),
})
const tabData = ref([
["editTab", "编辑源"],
["editDebug", "调试源"],
["editList", "源列表"],
["editHelp", "帮助信息"],
]);
['editTab', '编辑源'],
['editDebug', '调试源'],
['editList', '源列表'],
['editHelp', '帮助信息'],
])
</script>
<style lang="scss" scoped>
+142 -145
View File
@@ -36,7 +36,7 @@
<div class="hotkeys-settings flex-column-center">
<div
v-for="(button, index) in buttons"
v-for="(button, buttonIndex) in buttons"
:key="button.name"
class="hotkeys-item flex-space-between"
>
@@ -44,9 +44,9 @@
><el-text>{{ button.name }}</el-text></span
>
<div class="hotkeys-item__content">
<div v-for="(key, index) in button.hotKeys" :key="key">
<div v-for="(key, hotKeysIndex) in button.hotKeys" :key="key">
<kbd>{{ key }}</kbd>
<span v-if="index + 1 < button.hotKeys.length">
<span v-if="hotKeysIndex + 1 < button.hotKeys.length">
<el-text>+</el-text>
</span>
</div>
@@ -56,7 +56,7 @@
:disabled="recordKeyDowning"
text
:icon="Edit"
@click="recordKeyDown(index)"
@click="recordKeyDown(buttonIndex)"
>编辑</el-button
>
</div>
@@ -64,59 +64,59 @@
</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";
<script setup lang="ts">
import API from '@api'
import { CircleCheckFilled, Edit } from '@element-plus/icons-vue'
import hotkeys from 'hotkeys-js'
import { getSourceName, isInvaildSource } from '../utils/souce'
const store = useSourceStore();
const store = useSourceStore()
const pull = () => {
const loadingMsg = ElMessage({
message: "加载中……",
message: '加载中……',
showClose: true,
duration: 0,
});
})
API.getSources()
.then(({ data }) => {
if (data.isSuccess) {
store.changeTabName("editList");
store.saveSources(data.data);
store.changeTabName('editList')
store.saveSources(data.data)
ElMessage({
message: `成功拉取${data.data.length}条源`,
type: "success",
});
type: 'success',
})
} else {
ElMessage({
message: data.errorMsg ?? "后端错误",
type: "error",
});
message: data.errorMsg ?? '后端错误',
type: 'error',
})
}
})
.finally(() => loadingMsg.close());
};
.finally(() => loadingMsg.close())
}
const push = () => {
let sources = store.sources;
store.changeTabName("editList");
const sources = store.sources
store.changeTabName('editList')
if (sources.length === 0) {
return ElMessage({
message: "空空如也",
type: "info",
});
message: '空空如也',
type: 'info',
})
}
ElMessage({
message: "正在推送中",
type: "info",
});
message: '正在推送中',
type: 'info',
})
API.saveSources(sources).then(({ data }) => {
if (data.isSuccess) {
let okData = data.data;
const okData = data.data
if (Array.isArray(okData)) {
let failMsg = ``;
let failMsg = ``
if (sources.length > okData.length) {
failMsg = "\n推送失败的源将用红色字体标注!";
store.setPushReturnSources(okData);
failMsg = '\n推送失败的源将用红色字体标注!'
store.setPushReturnSources(okData)
}
ElMessage({
message: `批量推送源到「阅读3.0APP」\n共计: ${
@@ -124,168 +124,163 @@ const push = () => {
}\n成功: ${okData.length}\n失败: ${
sources.length - okData.length
}${failMsg}`,
type: "success",
});
type: 'success',
})
}
} else {
ElMessage({
message: `批量推送源失败!\nErrorMsg: ${data.errorMsg}`,
type: "error",
});
type: 'error',
})
}
});
};
})
}
const conver2Tab = () => {
store.changeTabName("editTab");
store.changeEditTabSource(store.currentSource);
};
store.changeTabName('editTab')
store.changeEditTabSource(store.currentSource)
}
const conver2Source = () => {
store.changeCurrentSource(store.editTabSource);
};
store.changeCurrentSource(store.editTabSource)
}
const undo = () => {
store.editHistoryUndo();
};
store.editHistoryUndo()
}
const clearEdit = () => {
store.clearEdit();
store.clearEdit()
ElMessage({
message: "已清除",
type: "success",
});
};
message: '已清除',
type: 'success',
})
}
const redo = () => {
store.clearEdit();
store.clearAllHistory();
store.clearEdit()
store.clearAllHistory()
ElMessage({
message: "已清除所有历史记录",
type: "success",
});
};
message: '已清除所有历史记录',
type: 'success',
})
}
const saveSource = () => {
let isBookSource = /bookSource/i.test(location.href),
/** @type {import("@/source.js").Source} */
source = store.currentSource;
const source = store.currentSource
if (isInvaildSource(source)) {
API.saveSource(source).then(({ data }) => {
const sourceName = getSourceName(source)
if (data.isSuccess) {
ElMessage({
message: `源《${
isBookSource ? source.bookSourceName : source.sourceName
}》已成功保存到「阅读3.0APP」`,
type: "success",
});
message: `源《${sourceName}》已成功保存到「阅读3.0APP」`,
type: 'success',
})
//save to store
store.saveCurrentSource();
store.saveCurrentSource()
} else {
ElMessage({
message: `源《${
isBookSource ? source.bookSourceName : source.sourceName
}》保存失败!\nErrorMsg: ${data.errorMsg}`,
type: "error",
});
message: `源《${sourceName}》保存失败!\nErrorMsg: ${data.errorMsg}`,
type: 'error',
})
}
});
})
} else {
ElMessage({
message: `请检查<必填>项是否全部填写`,
type: "error",
});
type: 'error',
})
}
};
}
const debug = () => {
store.startDebug();
};
store.startDebug()
}
const buttons = ref(
const buttons = ref<{ name: string; hotKeys: string[]; action: () => void }[]>(
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 },
{ 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 hotkeysDialogVisible = ref(true)
const recordKeyDowning = ref(false);
const recordKeyDowning = ref(false)
const recordKeyDownIndex = ref(-1);
const recordKeyDownIndex = ref(-1)
const stopRecordKeyDown = () => {
if (!recordKeyDowning.value) {
hotkeysDialogVisible.value = false;
hotkeysDialogVisible.value = false
}
recordKeyDowning.value = false;
};
recordKeyDowning.value = false
}
watch(
hotkeysDialogVisible,
(visibale) => {
visibale => {
if (!visibale) {
hotkeys.unbind("*");
readHotkeysConfig();
bindHotKeys();
return;
hotkeys.unbind('*')
readHotkeysConfig()
bindHotKeys()
return
}
readHotkeysConfig();
hotkeys.unbind();
readHotkeysConfig()
hotkeys.unbind()
/**监听按键 */
hotkeys("*", (event) => {
event.preventDefault();
let pressedKeys = hotkeys.getPressedKeyString();
if (pressedKeys.length == 1 && pressedKeys[0] == "esc") {
hotkeys('*', event => {
event.preventDefault()
const pressedKeys = hotkeys.getPressedKeyString()
if (pressedKeys.length == 1 && pressedKeys[0] == 'esc') {
//单独按下esc 不录入
return;
return
}
if (recordKeyDowning.value && recordKeyDownIndex.value > -1)
buttons.value[recordKeyDownIndex.value].hotKeys = pressedKeys;
});
buttons.value[recordKeyDownIndex.value].hotKeys = pressedKeys
})
},
{ immediate: true },
);
)
const recordKeyDown = (index) => {
recordKeyDowning.value = true;
const recordKeyDown = (index: number) => {
recordKeyDowning.value = true
ElMessage({
message: "按ESC键或者点击空白处结束录入",
type: "info",
});
buttons.value[index].hotKeys = [];
recordKeyDownIndex.value = index;
};
message: '按ESC键或者点击空白处结束录入',
type: 'info',
})
buttons.value[index].hotKeys = []
recordKeyDownIndex.value = index
}
const saveHotKeys = () => {
const hotKeysConfig = [];
const hotKeysConfig: string[][] = []
buttons.value.forEach(({ hotKeys }) => {
hotKeysConfig.push(hotKeys);
});
saveHotkeysConfig(hotKeysConfig);
hotkeysDialogVisible.value = false;
};
hotKeysConfig.push(hotKeys)
})
saveHotkeysConfig(hotKeysConfig)
hotkeysDialogVisible.value = false
}
const bindHotKeys = () => {
// hotkeys默认过滤INPUT SELECT TEXTAREA
hotkeys.filter = () => true;
hotkeys.filter = () => true
buttons.value.forEach(({ hotKeys, action }) => {
if (hotKeys.length == 0) return;
hotkeys(hotKeys.join("+"), (event) => {
event.preventDefault();
action.call(null);
});
});
};
const saveHotkeysConfig = (config) => {
localStorage.setItem("legado_web_hotkeys", JSON.stringify(config));
};
if (hotKeys.length == 0) return
hotkeys(hotKeys.join('+'), event => {
event.preventDefault()
action.call(null)
})
})
}
const saveHotkeysConfig = (config: string[][]) => {
localStorage.setItem('legado_web_hotkeys', JSON.stringify(config))
}
/**
* 读取快捷键配置
@@ -293,26 +288,28 @@ const saveHotkeysConfig = (config) => {
*/
function readHotkeysConfig() {
try {
const config = JSON.parse(localStorage.getItem("legado_web_hotkeys"));
if (!Array.isArray(config) || config.length == 0) return false;
buttons.value.forEach((button, index) => (button.hotKeys = config[index]));
return true;
const localStorageConfig = localStorage.getItem('legado_web_hotkeys')
if (localStorageConfig === null) return false
const config = JSON.parse(localStorageConfig)
if (!Array.isArray(config) || config.length == 0) return false
buttons.value.forEach((button, index) => (button.hotKeys = config[index]))
return true
} catch {
ElMessage({ message: "快捷键配置错误", type: "error" });
localStorage.removeItem("legado_web_hotkeys");
ElMessage({ message: '快捷键配置错误', type: 'error' })
localStorage.removeItem('legado_web_hotkeys')
}
return false;
return false
}
onMounted(() => {
/**读取热键配置 */
if (readHotkeysConfig()) {
hotkeysDialogVisible.value = false;
hotkeysDialogVisible.value = false
}
});
})
</script>
<style lang="scss" scoped>
<style scoped>
.flex-space-between {
display: flex;
justify-content: space-between;
@@ -337,7 +334,7 @@ onMounted(() => {
justify-content: flex-end;
margin-right: 1em;
}
&__content {
.hotkeys-item__content {
display: flex;
flex-wrap: wrap;
flex: 1;