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="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 class="toc-content" v-show="tocsExpand">
<div v-for="toc in articleToc" :key="toc.index" :class="[toc.clazz]" @click="toScroll(toc.level, toc.content)">
<span v-html="toc.content"></span>
</div>
<div v-for="toc in articleToc" :key="toc.id" :class="[toc.clazz]" @click="toScroll(toc.id)" v-html="toc.content"></div>
</div>
<div class="img-title">
引用图片
@ -171,7 +169,8 @@ import Notify from '@renderer/scripts/notify'
import { useDraggable } from '@renderer/scripts/draggable'
import type { shortcutFunc } from '@renderer/scripts/shortcut-register'
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 { useResize } from './scripts/editor-preview-resize'
// codemirror
@ -189,6 +188,8 @@ import marked, {
} from './scripts/markedjs'
import { EPScroll } from './scripts/editor-preview-scroll'
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 EditorTools = defineAsyncComponent(() => import('./EditorTools.vue'))
@ -463,7 +464,7 @@ const saveCurArticleContent = async (auto: boolean = false) => {
name: curArticle.value!.name,
markdown: cmw.getDocString(),
html: PreviewRef.value.innerHTML,
toc: JSON.stringify(articleToc.value),
// toc: JSON.stringify(articleToc.value),
references: articleImg.value.concat(articleLink.value)
}
await articleUpdContentApi(data)
@ -614,7 +615,7 @@ const setNewState = (md: string): void => {
//#region ----------------------------------------< marked/preview >-------------------------------
const renderInterval = ref(0) //
const articleHtml = ref('') // html
const articleHtml = shallowRef('') // html
let immediateParse = false // , ,
/**
* 自定义渲染
@ -633,7 +634,6 @@ const renderer = {
return renderCode(code, language, _isEscaped)
},
heading(text: any, level: number): string {
articleToc.value.push({ level: level, clazz: 'toc-' + level, index: articleToc.value.length, content: text })
return renderHeading(text, level)
},
image(href: string | null, _title: string | null, text: string): string {
@ -671,6 +671,7 @@ const parse = () => {
articleHtml.value = content
renderInterval.value = Date.now() - begin
articleParseing = false
nextTick(() => parseToc())
})
}
@ -693,9 +694,9 @@ useResize(EditorRef, PreviewRef, ResizeDividerRef)
//#endregion
//#region ----------------------------------------< TOC >------------------------------------------
const articleToc = ref<any[]>([])
const articleImg = ref<ArticleReference[]>([]) //
const articleLink = ref<ArticleReference[]>([]) //
const articleToc = shallowRef<Toc[]>([])
const articleImg = shallowRef<ArticleReference[]>([]) //
const articleLink = shallowRef<ArticleReference[]>([]) //
const TocRef = ref()
const TocTitleRef = ref()
/**
@ -703,18 +704,20 @@ const TocTitleRef = ref()
* @param level 标题级别
* @param content 标题内容
*/
const toScroll = (level: number, content: string) => {
let id = level + '-' + content
const toScroll = (id: string) => {
let elm: HTMLElement = document.getElementById(id) as HTMLElement
;(elm.parentNode as Element).scrollTop = elm.offsetTop
}
//
const clearTocAndImg = () => {
articleToc.value = []
articleImg.value = []
articleLink.value = []
}
const parseToc = async () => {
parseTocAsync(PreviewRef.value).then((tocs) => (articleToc.value = tocs))
}
useDraggable(TocRef, TocTitleRef)
//#endregion

View File

@ -4,60 +4,62 @@
<div class="bl-preview-toc-block">
<div class="toc-subtitle">{{ article?.name }}</div>
<div class="toc-subtitle">
<span class="iconbl bl-pen-line"></span> {{ article?.words }} |
<span class="iconbl bl-read-line"></span> {{ article?.uv }} |
<span class="iconbl bl-pen-line"></span> {{ article?.words }} | <span class="iconbl bl-read-line"></span> {{ article?.uv }} |
<span class="iconbl bl-like-line"></span> {{ article?.likes }}
</div>
<div class="toc-subtitle">
<span class="iconbl bl-a-clock3-line"></span> 公开 {{ article?.openTime }}
</div>
<div class="toc-subtitle">
<span class="iconbl bl-a-clock3-line"></span> 修改 {{ article?.updTime }}
</div>
<div class="toc-subtitle"><span class="iconbl bl-a-clock3-line"></span> 公开 {{ article?.openTime }}</div>
<div class="toc-subtitle"><span class="iconbl bl-a-clock3-line"></span> 修改 {{ article?.updTime }}</div>
<div class="toc-title">目录</div>
<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 }}
</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">
<div class="iconbl bl-send-line backtop"></div>
</el-backtop>
</div>
</template>
<script setup lang="ts">
import { storeToRefs } from "pinia"
import { ref, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { ref, onMounted, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { articleInfoApi } from '@renderer/api/blossom'
import { useConfigStore } from '@renderer/stores/config'
import { parseTocAsync } from './scripts/article'
import type { Toc } from './scripts/article'
const configStore = useConfigStore()
const { editorStyle } = storeToRefs(configStore)
const route = useRoute();
const route = useRoute()
const article = ref<DocInfo>()
const tocList = ref<any>([])
const tocs = ref<Toc[]>([])
const WindowPreviewRef = ref()
/**
* 跳转至指定ID位置,ID为 标题级别-标题内容
* @param level 标题级别
* @param content 标题内容
*/
const toScroll = (level: number, content: string) => {
let id = level + '-' + content
const toScroll = (id: string) => {
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) => {
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
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)
})
</script>
<style scoped lang=scss>
<style scoped lang="scss">
@import './styles/bl-preview-toc.scss';
@import './styles/article-backtop.scss';
@ -88,10 +90,7 @@ onMounted(() => {
:deep(.katex > *) {
font-size: 1.2em !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>
<el-tooltip
content="查看快捷键"
content="快捷键说明"
popper-class="is-small"
effect="light"
placement="top"

View File

@ -46,3 +46,52 @@ export interface ArticleReference {
*/
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
//#region ----------------------------------------< renderer >--------------------------------------
const domParser = new DOMParser()
/**
* TOC ,
* @param text
* @param level
*/
export const renderHeading = (text: any, level: number) => {
const realLevel = level
return `<h${realLevel} id="${realLevel}-${text}">${text}</h${realLevel}>`
let id: string = randomInt(1000000, 9999999).toString()
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
* @returns
*/
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>`
}

View File

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

View File

@ -113,7 +113,7 @@
</div>
<div class="toc-title">目录</div>
<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 }}
</div>
</div>
@ -185,7 +185,7 @@ const article = ref<DocInfo>({
html: `<div style="color:#E3E3E3;width:100%;height:300px;display:flex;justify-content: center;
align-items: center;font-size:25px;">请在左侧菜单选择文章</div>`
})
const tocList = ref<any>([])
const tocList = ref<Toc[]>([])
const defaultOpeneds = ref<string[]>([])
const PreviewRef = ref()
@ -217,6 +217,10 @@ const getDocTree = () => {
}
}
/**
* 获取文章信息
* @param tree
*/
const clickCurDoc = async (tree: DocTree) => {
// , ,
if (tree.ty == 3) {
@ -224,6 +228,7 @@ const clickCurDoc = async (tree: DocTree) => {
window.history.replaceState('', '', '#/articles?articleId=' + tree.i)
nextTick(() => {
PreviewRef.value.scrollTo({ top: 0 })
parseTocAsync(PreviewRef.value)
})
}
}
@ -240,19 +245,56 @@ const getCurEditArticle = async (id: number) => {
}
const then = (resp: any) => {
if (isNull(resp.data)) return
if (isNull(resp.data)) {
return
}
article.value = resp.data
tocList.value = JSON.parse(resp.data.toc)
}
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 {
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)
elm?.scrollIntoView(true)
}
@ -349,6 +391,9 @@ const closeAll = () => {
maskStyle.value = { display: 'none' }
}
/**
*
*/
const onresize = () => {
let width = document.body.clientWidth
if (width < 1100) {
@ -552,10 +597,6 @@ const onresize = () => {
.toc-5,
.toc-6 {
cursor: pointer;
// overflow: hidden;
// white-space: nowrap;
// text-overflow: ellipsis;
// white-space: pre;
&:hover {
font-weight: bold;
@ -564,7 +605,6 @@ const onresize = () => {
.toc-1 {
font-size: 1.1em;
border-top: 2px solid #eeeeee;
margin-top: 5px;
padding-top: 5px;

View File

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