mirror of
https://github.com/blossom-editor/blossom
synced 2024-11-17 14:39:21 +08:00
refactor: 目录的构造方式
This commit is contained in:
parent
71c7e8619a
commit
6f39fe9a5f
@ -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
|
||||
|
@ -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>
|
||||
|
@ -227,7 +227,7 @@
|
||||
<!-- 其他工具 -->
|
||||
<div class="divider"></div>
|
||||
<el-tooltip
|
||||
content="查看快捷键"
|
||||
content="快捷键说明"
|
||||
popper-class="is-small"
|
||||
effect="light"
|
||||
placement="top"
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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>`
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
9
blossom-web/src/views/article/index.d.ts
vendored
9
blossom-web/src/views/article/index.d.ts
vendored
@ -53,3 +53,12 @@ declare type DocType = 1 | 2 | 3
|
||||
declare interface Window {
|
||||
onHtmlEventDispatch: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 目录结构
|
||||
*/
|
||||
declare interface Toc {
|
||||
content: string
|
||||
clazz: string
|
||||
id: string
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user