refactor: 目录的构造方式

This commit is contained in:
xiaozzzi 2024-01-16 00:20:38 +08:00
parent 71c7e8619a
commit 6f39fe9a5f
8 changed files with 172 additions and 87 deletions

View File

@ -69,12 +69,10 @@
<div :class="['bl-preview-toc-absolute', tocsExpand ? 'is-expand-open' : 'is-expand-close']" ref="TocRef"> <div :class="['bl-preview-toc-absolute', tocsExpand ? 'is-expand-open' : 'is-expand-close']" ref="TocRef">
<div class="toc-title" ref="TocTitleRef"> <div class="toc-title" ref="TocTitleRef">
目录 目录
<span v-show="tocsExpand" style="font-size: 10px">({{ isMacOS() ? 'Cmd' : 'Alt' }}+2 可隐藏)</span> <span v-show="tocsExpand" style="font-size: 10px">({{ keymaps.hideToc }} 可隐藏)</span>
</div> </div>
<div class="toc-content" v-show="tocsExpand"> <div class="toc-content" v-show="tocsExpand">
<div v-for="toc in articleToc" :key="toc.index" :class="[toc.clazz]" @click="toScroll(toc.level, toc.content)"> <div v-for="toc in articleToc" :key="toc.id" :class="[toc.clazz]" @click="toScroll(toc.id)" v-html="toc.content"></div>
<span v-html="toc.content"></span>
</div>
</div> </div>
<div class="img-title"> <div class="img-title">
引用图片 引用图片
@ -171,7 +169,8 @@ import Notify from '@renderer/scripts/notify'
import { useDraggable } from '@renderer/scripts/draggable' import { useDraggable } from '@renderer/scripts/draggable'
import type { shortcutFunc } from '@renderer/scripts/shortcut-register' import type { shortcutFunc } from '@renderer/scripts/shortcut-register'
import { treeToInfo, provideKeyDocInfo, provideKeyCurArticleInfo } from '@renderer/views/doc/doc' import { treeToInfo, provideKeyDocInfo, provideKeyCurArticleInfo } from '@renderer/views/doc/doc'
import { TempTextareaKey, ArticleReference, DocEditorStyle } from './scripts/article' import { TempTextareaKey, ArticleReference, DocEditorStyle, parseTocAsync } from './scripts/article'
import type { Toc } from './scripts/article'
import { beforeUpload, onError, picCacheWrapper, picCacheRefresh, uploadForm, uploadDate } from '@renderer/views/picture/scripts/picture' import { beforeUpload, onError, picCacheWrapper, picCacheRefresh, uploadForm, uploadDate } from '@renderer/views/picture/scripts/picture'
import { useResize } from './scripts/editor-preview-resize' import { useResize } from './scripts/editor-preview-resize'
// codemirror // codemirror
@ -189,6 +188,8 @@ import marked, {
} from './scripts/markedjs' } from './scripts/markedjs'
import { EPScroll } from './scripts/editor-preview-scroll' import { EPScroll } from './scripts/editor-preview-scroll'
import { useArticleHtmlEvent } from './scripts/article-html-event' import { useArticleHtmlEvent } from './scripts/article-html-event'
import { shallowRef } from 'vue'
import { keymaps } from './scripts/editor-tools'
const PictureViewerInfo = defineAsyncComponent(() => import('@renderer/views/picture/PictureViewerInfo.vue')) const PictureViewerInfo = defineAsyncComponent(() => import('@renderer/views/picture/PictureViewerInfo.vue'))
// const EditorTools = defineAsyncComponent(() => import('./EditorTools.vue')) // const EditorTools = defineAsyncComponent(() => import('./EditorTools.vue'))
@ -463,7 +464,7 @@ const saveCurArticleContent = async (auto: boolean = false) => {
name: curArticle.value!.name, name: curArticle.value!.name,
markdown: cmw.getDocString(), markdown: cmw.getDocString(),
html: PreviewRef.value.innerHTML, html: PreviewRef.value.innerHTML,
toc: JSON.stringify(articleToc.value), // toc: JSON.stringify(articleToc.value),
references: articleImg.value.concat(articleLink.value) references: articleImg.value.concat(articleLink.value)
} }
await articleUpdContentApi(data) await articleUpdContentApi(data)
@ -614,7 +615,7 @@ const setNewState = (md: string): void => {
//#region ----------------------------------------< marked/preview >------------------------------- //#region ----------------------------------------< marked/preview >-------------------------------
const renderInterval = ref(0) // const renderInterval = ref(0) //
const articleHtml = ref('') // html const articleHtml = shallowRef('') // html
let immediateParse = false // , , let immediateParse = false // , ,
/** /**
* 自定义渲染 * 自定义渲染
@ -633,7 +634,6 @@ const renderer = {
return renderCode(code, language, _isEscaped) return renderCode(code, language, _isEscaped)
}, },
heading(text: any, level: number): string { heading(text: any, level: number): string {
articleToc.value.push({ level: level, clazz: 'toc-' + level, index: articleToc.value.length, content: text })
return renderHeading(text, level) return renderHeading(text, level)
}, },
image(href: string | null, _title: string | null, text: string): string { image(href: string | null, _title: string | null, text: string): string {
@ -671,6 +671,7 @@ const parse = () => {
articleHtml.value = content articleHtml.value = content
renderInterval.value = Date.now() - begin renderInterval.value = Date.now() - begin
articleParseing = false articleParseing = false
nextTick(() => parseToc())
}) })
} }
@ -693,9 +694,9 @@ useResize(EditorRef, PreviewRef, ResizeDividerRef)
//#endregion //#endregion
//#region ----------------------------------------< TOC >------------------------------------------ //#region ----------------------------------------< TOC >------------------------------------------
const articleToc = ref<any[]>([]) const articleToc = shallowRef<Toc[]>([])
const articleImg = ref<ArticleReference[]>([]) // const articleImg = shallowRef<ArticleReference[]>([]) //
const articleLink = ref<ArticleReference[]>([]) // const articleLink = shallowRef<ArticleReference[]>([]) //
const TocRef = ref() const TocRef = ref()
const TocTitleRef = ref() const TocTitleRef = ref()
/** /**
@ -703,18 +704,20 @@ const TocTitleRef = ref()
* @param level 标题级别 * @param level 标题级别
* @param content 标题内容 * @param content 标题内容
*/ */
const toScroll = (level: number, content: string) => { const toScroll = (id: string) => {
let id = level + '-' + content
let elm: HTMLElement = document.getElementById(id) as HTMLElement let elm: HTMLElement = document.getElementById(id) as HTMLElement
;(elm.parentNode as Element).scrollTop = elm.offsetTop ;(elm.parentNode as Element).scrollTop = elm.offsetTop
} }
// //
const clearTocAndImg = () => { const clearTocAndImg = () => {
articleToc.value = []
articleImg.value = [] articleImg.value = []
articleLink.value = [] articleLink.value = []
} }
const parseToc = async () => {
parseTocAsync(PreviewRef.value).then((tocs) => (articleToc.value = tocs))
}
useDraggable(TocRef, TocTitleRef) useDraggable(TocRef, TocTitleRef)
//#endregion //#endregion

View File

@ -4,60 +4,62 @@
<div class="bl-preview-toc-block"> <div class="bl-preview-toc-block">
<div class="toc-subtitle">{{ article?.name }}</div> <div class="toc-subtitle">{{ article?.name }}</div>
<div class="toc-subtitle"> <div class="toc-subtitle">
<span class="iconbl bl-pen-line"></span> {{ article?.words }} | <span class="iconbl bl-pen-line"></span> {{ article?.words }} | <span class="iconbl bl-read-line"></span> {{ article?.uv }} |
<span class="iconbl bl-read-line"></span> {{ article?.uv }} |
<span class="iconbl bl-like-line"></span> {{ article?.likes }} <span class="iconbl bl-like-line"></span> {{ article?.likes }}
</div> </div>
<div class="toc-subtitle"> <div class="toc-subtitle"><span class="iconbl bl-a-clock3-line"></span> 公开 {{ article?.openTime }}</div>
<span class="iconbl bl-a-clock3-line"></span> 公开 {{ article?.openTime }} <div class="toc-subtitle"><span class="iconbl bl-a-clock3-line"></span> 修改 {{ article?.updTime }}</div>
</div>
<div class="toc-subtitle">
<span class="iconbl bl-a-clock3-line"></span> 修改 {{ article?.updTime }}
</div>
<div class="toc-title">目录</div> <div class="toc-title">目录</div>
<div class="toc-content"> <div class="toc-content">
<div v-for="toc in tocList" :key="toc.index" :class="[toc.clazz]" @click="toScroll(toc.level, toc.content)"> <div v-for="toc in tocs" :key="toc.id" :class="[toc.clazz]" @click="toScroll(toc.id)">
{{ toc.content }} {{ toc.content }}
</div> </div>
</div> </div>
</div> </div>
<div class="preview bl-preview" :style="editorStyle" v-html="article?.html"></div> <div class="preview bl-preview" :style="editorStyle" v-html="article?.html" ref="WindowPreviewRef"></div>
<el-backtop target=".preview" :right="50" :bottom="50"> <el-backtop target=".preview" :right="50" :bottom="50">
<div class="iconbl bl-send-line backtop"></div> <div class="iconbl bl-send-line backtop"></div>
</el-backtop> </el-backtop>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { storeToRefs } from "pinia" import { storeToRefs } from 'pinia'
import { ref, onMounted } from 'vue' import { ref, onMounted, nextTick } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { articleInfoApi } from '@renderer/api/blossom' import { articleInfoApi } from '@renderer/api/blossom'
import { useConfigStore } from '@renderer/stores/config' import { useConfigStore } from '@renderer/stores/config'
import { parseTocAsync } from './scripts/article'
import type { Toc } from './scripts/article'
const configStore = useConfigStore() const configStore = useConfigStore()
const { editorStyle } = storeToRefs(configStore) const { editorStyle } = storeToRefs(configStore)
const route = useRoute(); const route = useRoute()
const article = ref<DocInfo>() const article = ref<DocInfo>()
const tocList = ref<any>([]) const tocs = ref<Toc[]>([])
const WindowPreviewRef = ref()
/** /**
* 跳转至指定ID位置,ID为 标题级别-标题内容 * 跳转至指定ID位置,ID为 标题级别-标题内容
* @param level 标题级别 * @param level 标题级别
* @param content 标题内容 * @param content 标题内容
*/ */
const toScroll = (level: number, content: string) => { const toScroll = (id: string) => {
let id = level + '-' + content
let elm: HTMLElement = document.getElementById(id) as HTMLElement let elm: HTMLElement = document.getElementById(id) as HTMLElement
(elm.parentNode as Element).scrollTop = elm.offsetTop ;(elm.parentNode as Element).scrollTop = elm.offsetTop - 40
} }
const initPreview = (articleId: string) => { const initPreview = (articleId: string) => {
articleInfoApi({ id: articleId, showToc: true, showMarkdown: false, showHtml: true }).then(resp => { articleInfoApi({ id: articleId, showToc: false, showMarkdown: false, showHtml: true }).then((resp) => {
article.value = resp.data article.value = resp.data
tocList.value = JSON.parse(resp.data.toc) nextTick(() => initToc())
})
}
const initToc = () => {
parseTocAsync(WindowPreviewRef.value).then((toc) => {
tocs.value = toc
}) })
} }
@ -65,7 +67,7 @@ onMounted(() => {
initPreview(route.query.articleId as string) initPreview(route.query.articleId as string)
}) })
</script> </script>
<style scoped lang=scss> <style scoped lang="scss">
@import './styles/bl-preview-toc.scss'; @import './styles/bl-preview-toc.scss';
@import './styles/article-backtop.scss'; @import './styles/article-backtop.scss';
@ -88,10 +90,7 @@ onMounted(() => {
:deep(.katex > *) { :deep(.katex > *) {
font-size: 1.2em !important; font-size: 1.2em !important;
font-family: 'KaTeX_Size1', sans-serif !important; font-family: 'KaTeX_Size1', sans-serif !important;
// font-size: 1.3em !important;
// font-family: 'KaTeX_Math', sans-serif !important;
} }
} }
} }
</style> </style>

View File

@ -227,7 +227,7 @@
<!-- 其他工具 --> <!-- 其他工具 -->
<div class="divider"></div> <div class="divider"></div>
<el-tooltip <el-tooltip
content="查看快捷键" content="快捷键说明"
popper-class="is-small" popper-class="is-small"
effect="light" effect="light"
placement="top" placement="top"

View File

@ -46,3 +46,52 @@ export interface ArticleReference {
*/ */
type: 10 | 11 | 12 | 21 type: 10 | 11 | 12 | 21
} }
/**
*
*/
export interface Toc {
content: string
clazz: string
id: string
}
/**
* ,
*
* @param ele
* @returns
*/
export const parseTocAsync = async (ele: HTMLElement): Promise<Toc[]> => {
let heads = ele.querySelectorAll('h1, h2, h3, h4, h5, h6')
let tocs: Toc[] = []
for (let i = 0; i < heads.length; i++) {
let head: Element = heads[i]
let level = 1
let content = (head as HTMLElement).innerText
let id = head.id
switch (head.localName) {
case 'h1':
level = 1
break
case 'h2':
level = 2
break
case 'h3':
level = 3
break
case 'h4':
level = 4
break
case 'h5':
level = 5
break
case 'h6':
level = 6
break
}
let toc: Toc = { content: content, clazz: 'toc-' + level, id: id }
tocs.push(toc)
}
return tocs
}

View File

@ -103,15 +103,26 @@ export const tokenizerCodespan = (src: string): any => {
//#endregion //#endregion
//#region ----------------------------------------< renderer >-------------------------------------- //#region ----------------------------------------< renderer >--------------------------------------
const domParser = new DOMParser()
/** /**
* TOC , * TOC ,
* @param text * @param text
* @param level * @param level
*/ */
export const renderHeading = (text: any, level: number) => { export const renderHeading = (text: any, level: number) => {
const realLevel = level let id: string = randomInt(1000000, 9999999).toString()
return `<h${realLevel} id="${realLevel}-${text}">${text}</h${realLevel}>` try {
let dom = domParser.parseFromString(text, 'text/html')
if (dom) {
id += dom.body.innerText
} else {
id += text
}
} catch {
id += text
}
return `<h${level} id="${id}">${text}</h${level}>`
} }
/** /**
@ -328,25 +339,10 @@ export const renderCode = (code: string, language: string | undefined, _isEscape
/** /**
* *
* 1. katex `$内部写表达式$`
* @param src * @param src
* @returns * @returns
*/ */
export const renderCodespan = (src: string) => { export const renderCodespan = (src: string) => {
let arr = src.match(singleDollar)
if (arr != null && arr.length > 0) {
try {
return katex.renderToString(arr[1], {
throwOnError: true,
output: 'html'
})
} catch (error) {
console.error(error)
return `<div class='bl-preview-analysis-fail-inline'>
Katex ! <a href='https://katex.org/#demo' target='_blank'> Katex </a>
</div>`
}
}
return `<code>${src}</code>` return `<code>${src}</code>`
} }

View File

@ -7,7 +7,6 @@
.toc-content { .toc-content {
overflow-y: overlay; overflow-y: overlay;
padding-top: 10px;
.toc-1, .toc-1,
.toc-2, .toc-2,
@ -38,33 +37,23 @@
} }
.toc-2 { .toc-2 {
&::before { padding-left: 10px;
content: ' ';
}
} }
.toc-3 { .toc-3 {
&::before { padding-left: 20px;
content: ' ';
}
} }
.toc-4 { .toc-4 {
&::before { padding-left: 30px;
content: ' ';
}
} }
.toc-5 { .toc-5 {
&::before { padding-left: 40px;
content: ' ';
}
} }
.toc-6 { .toc-6 {
&::before { padding-left: 50px;
content: ' ';
}
} }
} }
} }

View File

@ -113,7 +113,7 @@
</div> </div>
<div class="toc-title">目录</div> <div class="toc-title">目录</div>
<div class="toc-content"> <div class="toc-content">
<div v-for="toc in tocList" :key="toc.index" :class="[toc.clazz]" @click="toScroll(toc.level, toc.content)"> <div v-for="toc in tocList" :key="toc.id" :class="[toc.clazz]" @click="toScroll(toc.id)">
{{ toc.content }} {{ toc.content }}
</div> </div>
</div> </div>
@ -185,7 +185,7 @@ const article = ref<DocInfo>({
html: `<div style="color:#E3E3E3;width:100%;height:300px;display:flex;justify-content: center; html: `<div style="color:#E3E3E3;width:100%;height:300px;display:flex;justify-content: center;
align-items: center;font-size:25px;">请在左侧菜单选择文章</div>` align-items: center;font-size:25px;">请在左侧菜单选择文章</div>`
}) })
const tocList = ref<any>([]) const tocList = ref<Toc[]>([])
const defaultOpeneds = ref<string[]>([]) const defaultOpeneds = ref<string[]>([])
const PreviewRef = ref() const PreviewRef = ref()
@ -217,6 +217,10 @@ const getDocTree = () => {
} }
} }
/**
* 获取文章信息
* @param tree
*/
const clickCurDoc = async (tree: DocTree) => { const clickCurDoc = async (tree: DocTree) => {
// , , // , ,
if (tree.ty == 3) { if (tree.ty == 3) {
@ -224,6 +228,7 @@ const clickCurDoc = async (tree: DocTree) => {
window.history.replaceState('', '', '#/articles?articleId=' + tree.i) window.history.replaceState('', '', '#/articles?articleId=' + tree.i)
nextTick(() => { nextTick(() => {
PreviewRef.value.scrollTo({ top: 0 }) PreviewRef.value.scrollTo({ top: 0 })
parseTocAsync(PreviewRef.value)
}) })
} }
} }
@ -240,19 +245,56 @@ const getCurEditArticle = async (id: number) => {
} }
const then = (resp: any) => { const then = (resp: any) => {
if (isNull(resp.data)) return if (isNull(resp.data)) {
return
}
article.value = resp.data article.value = resp.data
tocList.value = JSON.parse(resp.data.toc)
} }
if (userStore.isLogin) { if (userStore.isLogin) {
await articleInfoApi({ id: id, showToc: true, showMarkdown: false, showHtml: true }).then((resp) => then(resp)) await articleInfoApi({ id: id, showToc: false, showMarkdown: false, showHtml: true }).then((resp) => then(resp))
} else { } else {
await articleInfoOpenApi({ id: id, showToc: true, showMarkdown: false, showHtml: true }).then((resp) => then(resp)) await articleInfoOpenApi({ id: id, showToc: false, showMarkdown: false, showHtml: true }).then((resp) => then(resp))
} }
} }
const toScroll = (level: number, content: string) => { /**
let id = level + '-' + content * 解析目录
*/
const parseTocAsync = async (ele: HTMLElement) => {
let heads = ele.querySelectorAll('h1, h2, h3, h4, h5, h6')
let tocs: Toc[] = []
for (let i = 0; i < heads.length; i++) {
let head: Element = heads[i]
let level = 1
let content = (head as HTMLElement).innerText
let id = head.id
switch (head.localName) {
case 'h1':
level = 1
break
case 'h2':
level = 2
break
case 'h3':
level = 3
break
case 'h4':
level = 4
break
case 'h5':
level = 5
break
case 'h6':
level = 6
break
}
let toc: Toc = { content: content, clazz: 'toc-' + level, id: id }
tocs.push(toc)
}
tocList.value = tocs
}
const toScroll = (id: string) => {
let elm = document.getElementById(id) let elm = document.getElementById(id)
elm?.scrollIntoView(true) elm?.scrollIntoView(true)
} }
@ -349,6 +391,9 @@ const closeAll = () => {
maskStyle.value = { display: 'none' } maskStyle.value = { display: 'none' }
} }
/**
*
*/
const onresize = () => { const onresize = () => {
let width = document.body.clientWidth let width = document.body.clientWidth
if (width < 1100) { if (width < 1100) {
@ -552,10 +597,6 @@ const onresize = () => {
.toc-5, .toc-5,
.toc-6 { .toc-6 {
cursor: pointer; cursor: pointer;
// overflow: hidden;
// white-space: nowrap;
// text-overflow: ellipsis;
// white-space: pre;
&:hover { &:hover {
font-weight: bold; font-weight: bold;
@ -564,7 +605,6 @@ const onresize = () => {
.toc-1 { .toc-1 {
font-size: 1.1em; font-size: 1.1em;
border-top: 2px solid #eeeeee;
margin-top: 5px; margin-top: 5px;
padding-top: 5px; padding-top: 5px;

View File

@ -53,3 +53,12 @@ declare type DocType = 1 | 2 | 3
declare interface Window { declare interface Window {
onHtmlEventDispatch: any onHtmlEventDispatch: any
} }
/**
*
*/
declare interface Toc {
content: string
clazz: string
id: string
}