@tnotesjs/core 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/config/defaultConfig.ts +33 -4
- package/config/index.ts +6 -0
- package/core/NoteIndexCache.ts +7 -1
- package/core/NoteManager.ts +18 -30
- package/dist/{chunk-5Y5IYS6C.js → chunk-UJG3UU5X.js} +21 -5
- package/dist/{chunk-5QN44ES5.js → chunk-XCWFZLER.js} +48 -368
- package/dist/cli/index.js +26 -78
- package/dist/index.js +1 -1
- package/dist/vitepress/config/index.js +159 -18
- package/package.json +3 -6
- package/services/index.ts +0 -1
- package/services/init-sub-repo/initSubRepoLogic.ts +0 -1
- package/services/note/service.ts +0 -1
- package/types/config.ts +0 -3
- package/types/note.ts +0 -2
- package/vitepress/assets/icons/icon__cursor.svg +4 -0
- package/vitepress/assets/icons/index.ts +1 -0
- package/vitepress/components/Layout/AboutPanel.vue +0 -24
- package/vitepress/components/Layout/DocBeforeControls.vue +6 -14
- package/vitepress/components/Layout/DocFooter.vue +3 -3
- package/vitepress/components/Layout/Layout.vue +0 -30
- package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +20 -11
- package/vitepress/components/Layout/homeReadme.data.ts +0 -48
- package/vitepress/components/MindmapPreview/InlineRuns.ts +25 -0
- package/vitepress/components/MindmapPreview/MindmapOutlineNode.vue +62 -0
- package/vitepress/components/MindmapPreview/MindmapPreview.vue +287 -0
- package/vitepress/components/MindmapPreview/compat.test.ts +93 -0
- package/vitepress/components/MindmapPreview/compat.ts +128 -0
- package/vitepress/components/MindmapPreview/expandLevel.test.ts +40 -0
- package/vitepress/components/MindmapPreview/expandLevel.ts +28 -0
- package/vitepress/components/MindmapPreview/legacyCorpus.test.ts +57 -0
- package/vitepress/components/Settings/Settings.vue +76 -73
- package/vitepress/components/SidebarCard/FolderTreeItems.vue +16 -6
- package/vitepress/components/SidebarCard/SidebarCard.vue +21 -27
- package/vitepress/components/composables/useLocalIde.ts +83 -0
- package/vitepress/components/constants.ts +5 -6
- package/vitepress/components/utils/vscodePaths.test.ts +22 -0
- package/vitepress/components/utils/vscodePaths.ts +24 -2
- package/vitepress/configs/markdown.config.ts +77 -20
- package/vitepress/theme/index.ts +4 -2
- package/vitepress/theme/styles/doc-layout.scss +4 -4
- package/vitepress/theme/styles/index.scss +0 -1
- package/services/timestamp/index.ts +0 -7
- package/services/timestamp/service.ts +0 -465
- package/vitepress/components/MarkMap/MarkMap.vue +0 -625
- package/vitepress/theme/styles/components/markmap.scss +0 -122
|
@@ -4,7 +4,7 @@ vitepress/components/Layout/DocFooter.vue
|
|
|
4
4
|
|
|
5
5
|
<template>
|
|
6
6
|
<nav class="custom-doc-footer" v-if="prev || next">
|
|
7
|
-
<div class="container">
|
|
7
|
+
<div class="pager-container">
|
|
8
8
|
<div class="prev">
|
|
9
9
|
<a v-if="prev" :href="prev.link" class="pager-link prev-link">
|
|
10
10
|
<span class="desc">上一篇</span>
|
|
@@ -147,7 +147,7 @@ const next = computed(() => {
|
|
|
147
147
|
padding-top: 32px;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
.container {
|
|
150
|
+
.pager-container {
|
|
151
151
|
display: flex;
|
|
152
152
|
gap: 16px;
|
|
153
153
|
}
|
|
@@ -219,7 +219,7 @@ const next = computed(() => {
|
|
|
219
219
|
|
|
220
220
|
/* 响应式调整 */
|
|
221
221
|
@media (max-width: 768px) {
|
|
222
|
-
.container {
|
|
222
|
+
.pager-container {
|
|
223
223
|
flex-direction: column;
|
|
224
224
|
gap: 12px;
|
|
225
225
|
}
|
|
@@ -32,10 +32,6 @@ vitepress/components/Layout/Layout.vue
|
|
|
32
32
|
:vscode-notes-dir="vscodeNotesDir"
|
|
33
33
|
:is-home-readme="isHomeReadme"
|
|
34
34
|
:current-note-id="currentNoteId"
|
|
35
|
-
:created_at="created_at"
|
|
36
|
-
:updated_at="updated_at"
|
|
37
|
-
:home-readme-created-at="homeReadmeCreatedAt"
|
|
38
|
-
:home-readme-updated-at="homeReadmeUpdatedAt"
|
|
39
35
|
:time-modal-open="timeModalOpen"
|
|
40
36
|
:all-collapsed="allCollapsed"
|
|
41
37
|
@open-time-modal="openTimeModal"
|
|
@@ -65,8 +61,6 @@ vitepress/components/Layout/Layout.vue
|
|
|
65
61
|
v-model:editable-note-status="editableNoteStatus"
|
|
66
62
|
v-model:editable-discussions-enabled="editableDiscussionsEnabled"
|
|
67
63
|
v-model:title-error="titleError"
|
|
68
|
-
:modal-created-at="modalCreatedAt"
|
|
69
|
-
:modal-updated-at="modalUpdatedAt"
|
|
70
64
|
:modal-github-url="modalGithubUrl"
|
|
71
65
|
:modal-github-page-url="modalGithubPageUrl"
|
|
72
66
|
:completion-percentage="completionPercentage"
|
|
@@ -116,10 +110,6 @@ vitepress/components/Layout/Layout.vue
|
|
|
116
110
|
</AboutModal>
|
|
117
111
|
</template>
|
|
118
112
|
<template #doc-footer-before>
|
|
119
|
-
<!-- <div class="footer-time-info">
|
|
120
|
-
<p title="首次提交时间">首次提交时间:{{ formatDate(created_at) }}</p>
|
|
121
|
-
<p title="最近提交时间">最近提交时间:{{ formatDate(updated_at) }}</p>
|
|
122
|
-
</div> -->
|
|
123
113
|
</template>
|
|
124
114
|
<template #doc-after>
|
|
125
115
|
<!-- 自定义 DocFooter -->
|
|
@@ -335,8 +325,6 @@ function getEmptyNoteConfig() {
|
|
|
335
325
|
const isDiscussionsVisible = computed(
|
|
336
326
|
() => currentNoteConfig.value.enableDiscussions,
|
|
337
327
|
);
|
|
338
|
-
const updated_at = computed(() => currentNoteConfig.value.updated_at);
|
|
339
|
-
const created_at = computed(() => currentNoteConfig.value.created_at);
|
|
340
328
|
|
|
341
329
|
// 判断是否为首页 README.md
|
|
342
330
|
const isHomeReadme = computed(() => vpData.page.value.filePath === "README.md");
|
|
@@ -349,10 +337,6 @@ const completionPercentage = computed(() => {
|
|
|
349
337
|
return Math.round((doneNotesLen.value / totalNotesLen.value) * 100);
|
|
350
338
|
});
|
|
351
339
|
|
|
352
|
-
// 首页 README.md 的时间戳
|
|
353
|
-
const homeReadmeCreatedAt = computed(() => readmeData?.created_at);
|
|
354
|
-
const homeReadmeUpdatedAt = computed(() => readmeData?.updated_at);
|
|
355
|
-
|
|
356
340
|
// #region - Composables
|
|
357
341
|
// 404 重定向
|
|
358
342
|
const { showNotFound, decodedCurrentPath, initRedirectCheck } =
|
|
@@ -509,20 +493,6 @@ const modalGithubPageUrl = computed(() => {
|
|
|
509
493
|
return "";
|
|
510
494
|
});
|
|
511
495
|
|
|
512
|
-
// modal 中显示的创建时间
|
|
513
|
-
const modalCreatedAt = computed(() => {
|
|
514
|
-
return modalIsHomeReadme.value
|
|
515
|
-
? homeReadmeCreatedAt.value
|
|
516
|
-
: modalNoteConfig.value.created_at;
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
// modal 中显示的更新时间
|
|
520
|
-
const modalUpdatedAt = computed(() => {
|
|
521
|
-
return modalIsHomeReadme.value
|
|
522
|
-
? homeReadmeUpdatedAt.value
|
|
523
|
-
: modalNoteConfig.value.updated_at;
|
|
524
|
-
});
|
|
525
|
-
|
|
526
496
|
function openTimeModal() {
|
|
527
497
|
sidebarAboutNoteId.value = null;
|
|
528
498
|
timeModalOpen.value = true;
|
|
@@ -1,40 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* vitepress/components/Layout/composables/useVSCodeIntegration.ts
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 本地 IDE 集成和 GitHub 链接拦截
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { useData } from 'vitepress'
|
|
8
|
-
import { ref } from 'vue'
|
|
8
|
+
import { computed, ref, watch } from 'vue'
|
|
9
9
|
|
|
10
|
+
import { useLocalIde } from '../../composables/useLocalIde'
|
|
10
11
|
import { NOTES_DIR_KEY } from '../../constants'
|
|
11
|
-
import {
|
|
12
|
-
resolveNoteReadmePath,
|
|
13
|
-
toVscodeFileUrl,
|
|
14
|
-
} from '../../utils/vscodePaths'
|
|
12
|
+
import { resolveNoteReadmePath, toIdeFileUrl } from '../../utils/vscodePaths'
|
|
15
13
|
|
|
16
14
|
import type { Router } from 'vitepress'
|
|
17
15
|
import type { Ref, ComputedRef } from 'vue'
|
|
18
16
|
|
|
19
|
-
|
|
20
17
|
/**
|
|
21
|
-
*
|
|
18
|
+
* 本地 IDE 集成和 GitHub 链接拦截
|
|
22
19
|
*/
|
|
23
20
|
export function useVSCodeIntegration() {
|
|
24
21
|
const vpData = useData()
|
|
25
|
-
const
|
|
22
|
+
const { ide } = useLocalIde()
|
|
23
|
+
const noteLocalPath = ref('')
|
|
24
|
+
|
|
25
|
+
const vscodeNotesDir = computed(() => {
|
|
26
|
+
if (!noteLocalPath.value) return ''
|
|
27
|
+
// 显式依赖 ide,确保切换 IDE 后 href 更新
|
|
28
|
+
return toIdeFileUrl(noteLocalPath.value, ide.value)
|
|
29
|
+
})
|
|
26
30
|
|
|
27
|
-
//
|
|
31
|
+
// 更新当前页面对应的本地笔记路径
|
|
28
32
|
const updateVscodeNoteDir = () => {
|
|
29
33
|
if (typeof window !== 'undefined') {
|
|
30
34
|
const notesDir = localStorage.getItem(NOTES_DIR_KEY)
|
|
31
35
|
const notePath = notesDir
|
|
32
36
|
? resolveNoteReadmePath(notesDir, vpData.page.value.relativePath)
|
|
33
37
|
: null
|
|
34
|
-
|
|
38
|
+
noteLocalPath.value = notePath || ''
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
|
|
42
|
+
watch(ide, () => {
|
|
43
|
+
// IDE 切换后重新解析路径(无需刷新页面)
|
|
44
|
+
updateVscodeNoteDir()
|
|
45
|
+
})
|
|
46
|
+
|
|
38
47
|
// 拦截 home README 中的笔记链接,将 GitHub 链接转换为站点内跳转
|
|
39
48
|
const interceptHomeReadmeLinks = (
|
|
40
49
|
isHomeReadme: Ref<boolean> | ComputedRef<boolean>,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* vitepress/components/Layout/homeReadme.data.ts
|
|
3
3
|
*/
|
|
4
|
-
import { execSync } from 'node:child_process'
|
|
5
4
|
import fs from 'node:fs'
|
|
6
5
|
import path from 'node:path'
|
|
7
6
|
|
|
@@ -12,8 +11,6 @@ interface ReadmeData {
|
|
|
12
11
|
doneNotesID: string[]
|
|
13
12
|
doneNotesLen: number
|
|
14
13
|
totalNotesLen: number
|
|
15
|
-
created_at?: number
|
|
16
|
-
updated_at?: number
|
|
17
14
|
}
|
|
18
15
|
|
|
19
16
|
export default {
|
|
@@ -28,15 +25,10 @@ export default {
|
|
|
28
25
|
|
|
29
26
|
watchedFiles.forEach((file) => {
|
|
30
27
|
if (file.endsWith('README.md')) {
|
|
31
|
-
// console.log('file:', file) // => file: README.md
|
|
32
|
-
|
|
33
28
|
const fileContent = fs.readFileSync(file, 'utf-8')
|
|
34
29
|
const doneNotesID = getDoneNotesID(fileContent)
|
|
35
30
|
const doneNotesLen = doneNotesID.length
|
|
36
31
|
|
|
37
|
-
// 获取 git 仓库的时间戳
|
|
38
|
-
const timestamps = getGitTimestamps()
|
|
39
|
-
|
|
40
32
|
// 计算总笔记数
|
|
41
33
|
const notesDir = path.join(path.dirname(file), 'notes')
|
|
42
34
|
const totalNotesLen = getTotalNotesCount(notesDir)
|
|
@@ -46,7 +38,6 @@ export default {
|
|
|
46
38
|
doneNotesID,
|
|
47
39
|
doneNotesLen,
|
|
48
40
|
totalNotesLen,
|
|
49
|
-
...timestamps,
|
|
50
41
|
}
|
|
51
42
|
}
|
|
52
43
|
})
|
|
@@ -74,45 +65,6 @@ function getTotalNotesCount(notesDir: string): number {
|
|
|
74
65
|
}
|
|
75
66
|
}
|
|
76
67
|
|
|
77
|
-
/**
|
|
78
|
-
* 获取 git 仓库的时间戳
|
|
79
|
-
* @returns 包含 created_at 和 updated_at 的对象
|
|
80
|
-
*/
|
|
81
|
-
function getGitTimestamps(): {
|
|
82
|
-
created_at?: number
|
|
83
|
-
updated_at?: number
|
|
84
|
-
} {
|
|
85
|
-
const now = Date.now()
|
|
86
|
-
let created_at = now
|
|
87
|
-
let updated_at = now
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
// 仓库的首次提交时间(最早的提交)
|
|
91
|
-
// 先获取所有提交,然后取第一行
|
|
92
|
-
const createdStdout = execSync(`git log --reverse --format=%ct`, {
|
|
93
|
-
encoding: 'utf-8',
|
|
94
|
-
})
|
|
95
|
-
const createdTs = createdStdout.toString().trim().split('\n')[0]
|
|
96
|
-
if (createdTs) {
|
|
97
|
-
created_at = parseInt(createdTs, 10) * 1000
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// 仓库的最近一次提交时间
|
|
101
|
-
const updatedStdout = execSync(`git log -1 --format=%ct`, {
|
|
102
|
-
encoding: 'utf-8',
|
|
103
|
-
})
|
|
104
|
-
const updatedTs = updatedStdout.toString().trim()
|
|
105
|
-
if (updatedTs) {
|
|
106
|
-
updated_at = parseInt(updatedTs, 10) * 1000
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return { created_at, updated_at }
|
|
110
|
-
} catch (error) {
|
|
111
|
-
console.error(`获取 git 仓库时间戳失败:`, error)
|
|
112
|
-
return { created_at, updated_at }
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
68
|
/**
|
|
117
69
|
* 返回已完成的笔记的 ID 列表
|
|
118
70
|
* @param fileContent 文件内容
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { parseInlineSegments } from '@tnotesjs/mindmap-core'
|
|
2
|
+
import { defineComponent, h } from 'vue'
|
|
3
|
+
|
|
4
|
+
export default defineComponent({
|
|
5
|
+
name: 'MindmapInlineRuns',
|
|
6
|
+
props: {
|
|
7
|
+
raw: { type: String, default: '' },
|
|
8
|
+
},
|
|
9
|
+
setup(props) {
|
|
10
|
+
return () => h('span', { class: 'mindmap-inline-runs' }, parseInlineSegments(props.raw).map((segment) => {
|
|
11
|
+
const classes = Object.entries(segment.marks)
|
|
12
|
+
.filter(([, enabled]) => enabled)
|
|
13
|
+
.map(([name]) => `is-${name}`)
|
|
14
|
+
if (segment.link) {
|
|
15
|
+
return h('a', {
|
|
16
|
+
class: ['mindmap-inline-run', 'is-link', ...classes],
|
|
17
|
+
href: segment.link.url,
|
|
18
|
+
target: '_blank',
|
|
19
|
+
rel: 'noopener noreferrer',
|
|
20
|
+
}, segment.text)
|
|
21
|
+
}
|
|
22
|
+
return h('span', { class: ['mindmap-inline-run', ...classes] }, segment.text)
|
|
23
|
+
}))
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import InlineRuns from './InlineRuns'
|
|
3
|
+
|
|
4
|
+
import type { MindmapNode } from '@tnotesjs/mindmap-core'
|
|
5
|
+
|
|
6
|
+
defineOptions({ name: 'MindmapOutlineNode' })
|
|
7
|
+
|
|
8
|
+
defineProps<{
|
|
9
|
+
node: MindmapNode
|
|
10
|
+
version: number
|
|
11
|
+
root?: boolean
|
|
12
|
+
}>()
|
|
13
|
+
|
|
14
|
+
defineEmits<{
|
|
15
|
+
toggle: [id: string]
|
|
16
|
+
}>()
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<li class="mindmap-outline-node" :class="{ 'is-root': root, 'is-done': node.content.checked === true }">
|
|
21
|
+
<div class="mindmap-outline-row">
|
|
22
|
+
<button
|
|
23
|
+
v-if="node.children.length > 0"
|
|
24
|
+
type="button"
|
|
25
|
+
class="mindmap-outline-toggle"
|
|
26
|
+
:aria-label="node.collapsed ? '展开主题' : '折叠主题'"
|
|
27
|
+
:aria-expanded="!node.collapsed"
|
|
28
|
+
@click="$emit('toggle', node.id)"
|
|
29
|
+
>
|
|
30
|
+
{{ node.collapsed ? '›' : '⌄' }}
|
|
31
|
+
</button>
|
|
32
|
+
<span v-else class="mindmap-outline-leaf" aria-hidden="true">•</span>
|
|
33
|
+
<input
|
|
34
|
+
v-if="node.content.checked !== null && !root"
|
|
35
|
+
class="mindmap-outline-checkbox"
|
|
36
|
+
type="checkbox"
|
|
37
|
+
:checked="node.content.checked"
|
|
38
|
+
disabled
|
|
39
|
+
aria-label="只读待办状态"
|
|
40
|
+
/>
|
|
41
|
+
<span class="mindmap-outline-label">
|
|
42
|
+
<InlineRuns :raw="node.content.image ? node.content.text : node.content.raw" />
|
|
43
|
+
</span>
|
|
44
|
+
</div>
|
|
45
|
+
<img
|
|
46
|
+
v-if="node.content.image"
|
|
47
|
+
class="mindmap-outline-image"
|
|
48
|
+
:src="node.content.image.src"
|
|
49
|
+
:alt="node.content.image.alt"
|
|
50
|
+
:style="node.content.image.width ? { width: `${node.content.image.width}px` } : undefined"
|
|
51
|
+
/>
|
|
52
|
+
<ul v-if="node.children.length > 0 && !node.collapsed" class="mindmap-outline-children">
|
|
53
|
+
<MindmapOutlineNode
|
|
54
|
+
v-for="child in node.children"
|
|
55
|
+
:key="child.id"
|
|
56
|
+
:node="child"
|
|
57
|
+
:version="version"
|
|
58
|
+
@toggle="$emit('toggle', $event)"
|
|
59
|
+
/>
|
|
60
|
+
</ul>
|
|
61
|
+
</li>
|
|
62
|
+
</template>
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { CanvasViewer, MindmapSession } from '@tnotesjs/mindmap-core'
|
|
3
|
+
import { onContentUpdated, useData } from 'vitepress'
|
|
4
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
|
|
5
|
+
|
|
6
|
+
import { normalizeMindmapMarkdown } from './compat'
|
|
7
|
+
import { applyInitialExpandLevel } from './expandLevel'
|
|
8
|
+
import MindmapOutlineNode from './MindmapOutlineNode.vue'
|
|
9
|
+
|
|
10
|
+
type PreviewView = 'mindmap' | 'outline' | 'source'
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(defineProps<{
|
|
13
|
+
content?: string
|
|
14
|
+
initialExpandLevel?: number
|
|
15
|
+
}>(), {
|
|
16
|
+
content: '',
|
|
17
|
+
initialExpandLevel: 3,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const { isDark } = useData()
|
|
21
|
+
const activeView = ref<PreviewView>('mindmap')
|
|
22
|
+
const canvasHost = ref<HTMLElement | null>(null)
|
|
23
|
+
const session = shallowRef<MindmapSession | null>(null)
|
|
24
|
+
const renderVersion = ref(0)
|
|
25
|
+
let viewer: CanvasViewer | null = null
|
|
26
|
+
let mounted = false
|
|
27
|
+
|
|
28
|
+
function decodeContent(value: string): string {
|
|
29
|
+
try {
|
|
30
|
+
return decodeURIComponent(value)
|
|
31
|
+
} catch {
|
|
32
|
+
return value
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const normalizedContent = computed(() => normalizeMindmapMarkdown(decodeContent(props.content)))
|
|
37
|
+
|
|
38
|
+
function createViewer(): void {
|
|
39
|
+
if (!mounted || !canvasHost.value || !session.value || viewer) return
|
|
40
|
+
viewer = new CanvasViewer(canvasHost.value, session.value, {
|
|
41
|
+
theme: isDark.value ? 'dark' : 'light',
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function rebuildSession(): void {
|
|
46
|
+
viewer?.destroy()
|
|
47
|
+
viewer = null
|
|
48
|
+
const next = new MindmapSession({
|
|
49
|
+
markdown: normalizedContent.value,
|
|
50
|
+
fileName: 'mindmap-preview.tn-mindmap.md',
|
|
51
|
+
})
|
|
52
|
+
applyInitialExpandLevel(next, props.initialExpandLevel)
|
|
53
|
+
const invalidate = () => { renderVersion.value += 1 }
|
|
54
|
+
next.on('collapseChange', invalidate)
|
|
55
|
+
next.on('focusChange', invalidate)
|
|
56
|
+
next.on('change', invalidate)
|
|
57
|
+
session.value = next
|
|
58
|
+
renderVersion.value += 1
|
|
59
|
+
void nextTick(createViewer)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function setView(view: PreviewView): void {
|
|
63
|
+
activeView.value = view
|
|
64
|
+
if (view === 'mindmap') void nextTick(() => viewer?.zoomToFit())
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function toggleNode(id: string): void {
|
|
68
|
+
session.value?.toggleCollapse(id)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function exitFocusTo(index: number): void {
|
|
72
|
+
session.value?.exitFocusTo(index)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
watch([normalizedContent, () => props.initialExpandLevel], rebuildSession, { immediate: true })
|
|
76
|
+
watch(isDark, (dark) => viewer?.setTheme(dark ? 'dark' : 'light'))
|
|
77
|
+
|
|
78
|
+
onMounted(() => {
|
|
79
|
+
mounted = true
|
|
80
|
+
createViewer()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
onContentUpdated(() => {
|
|
84
|
+
if (activeView.value === 'mindmap') void nextTick(() => viewer?.zoomToFit())
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
onBeforeUnmount(() => {
|
|
88
|
+
mounted = false
|
|
89
|
+
viewer?.destroy()
|
|
90
|
+
viewer = null
|
|
91
|
+
})
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<template>
|
|
95
|
+
<section class="mindmap-preview" :class="{ 'is-dark': isDark }">
|
|
96
|
+
<header class="mindmap-preview-header">
|
|
97
|
+
<nav class="mindmap-preview-tabs" aria-label="脑图预览视图">
|
|
98
|
+
<button
|
|
99
|
+
v-for="item in ([['mindmap', '脑图'], ['outline', '大纲'], ['source', '源码']] as const)"
|
|
100
|
+
:key="item[0]"
|
|
101
|
+
type="button"
|
|
102
|
+
:class="{ 'is-active': activeView === item[0] }"
|
|
103
|
+
@click="setView(item[0])"
|
|
104
|
+
>
|
|
105
|
+
{{ item[1] }}
|
|
106
|
+
</button>
|
|
107
|
+
</nav>
|
|
108
|
+
<button v-if="activeView === 'mindmap'" type="button" class="mindmap-fit" @click="viewer?.zoomToFit()">
|
|
109
|
+
适应视口
|
|
110
|
+
</button>
|
|
111
|
+
</header>
|
|
112
|
+
|
|
113
|
+
<nav v-if="session && session.focusPath.length > 0" class="mindmap-focus-path" aria-label="当前主题路径">
|
|
114
|
+
<button type="button" @click="exitFocusTo(0)">全部</button>
|
|
115
|
+
<template v-for="(node, index) in session.focusPath" :key="node.id">
|
|
116
|
+
<span aria-hidden="true">/</span>
|
|
117
|
+
<button type="button" @click="exitFocusTo(index + 1)">{{ node.content.text }}</button>
|
|
118
|
+
</template>
|
|
119
|
+
</nav>
|
|
120
|
+
|
|
121
|
+
<div v-show="activeView === 'mindmap'" ref="canvasHost" class="mindmap-canvas-host" />
|
|
122
|
+
|
|
123
|
+
<div v-if="activeView === 'outline' && session" class="mindmap-outline" :data-version="renderVersion">
|
|
124
|
+
<ul class="mindmap-outline-root">
|
|
125
|
+
<MindmapOutlineNode
|
|
126
|
+
:node="session.focusRootNode"
|
|
127
|
+
:version="renderVersion"
|
|
128
|
+
root
|
|
129
|
+
@toggle="toggleNode"
|
|
130
|
+
/>
|
|
131
|
+
</ul>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<pre v-if="activeView === 'source'" class="mindmap-source"><code>{{ normalizedContent }}</code></pre>
|
|
135
|
+
</section>
|
|
136
|
+
</template>
|
|
137
|
+
|
|
138
|
+
<style scoped lang="scss">
|
|
139
|
+
.mindmap-preview {
|
|
140
|
+
--mindmap-panel: var(--vp-c-bg-soft);
|
|
141
|
+
--mindmap-border: var(--vp-c-divider);
|
|
142
|
+
margin: 1.5rem 0;
|
|
143
|
+
overflow: hidden;
|
|
144
|
+
border: 1px solid var(--mindmap-border);
|
|
145
|
+
border-radius: 10px;
|
|
146
|
+
background: var(--vp-c-bg);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.mindmap-preview-header {
|
|
150
|
+
display: flex;
|
|
151
|
+
align-items: center;
|
|
152
|
+
justify-content: space-between;
|
|
153
|
+
min-height: 42px;
|
|
154
|
+
padding: 5px 8px;
|
|
155
|
+
border-bottom: 1px solid var(--mindmap-border);
|
|
156
|
+
background: var(--mindmap-panel);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.mindmap-preview-tabs {
|
|
160
|
+
display: flex;
|
|
161
|
+
gap: 3px;
|
|
162
|
+
|
|
163
|
+
button {
|
|
164
|
+
padding: 5px 12px;
|
|
165
|
+
border-radius: 6px;
|
|
166
|
+
color: var(--vp-c-text-2);
|
|
167
|
+
font-size: 13px;
|
|
168
|
+
font-weight: 600;
|
|
169
|
+
|
|
170
|
+
&:hover,
|
|
171
|
+
&.is-active {
|
|
172
|
+
color: var(--vp-c-text-1);
|
|
173
|
+
background: var(--vp-c-bg);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.mindmap-fit,
|
|
179
|
+
.mindmap-focus-path button {
|
|
180
|
+
color: var(--vp-c-text-2);
|
|
181
|
+
font-size: 12px;
|
|
182
|
+
|
|
183
|
+
&:hover { color: var(--vp-c-brand-1); }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.mindmap-focus-path {
|
|
187
|
+
display: flex;
|
|
188
|
+
gap: 6px;
|
|
189
|
+
align-items: center;
|
|
190
|
+
padding: 6px 12px;
|
|
191
|
+
overflow-x: auto;
|
|
192
|
+
border-bottom: 1px solid var(--mindmap-border);
|
|
193
|
+
white-space: nowrap;
|
|
194
|
+
color: var(--vp-c-text-3);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.mindmap-canvas-host {
|
|
198
|
+
position: relative;
|
|
199
|
+
width: 100%;
|
|
200
|
+
height: 440px;
|
|
201
|
+
overflow: hidden;
|
|
202
|
+
background: var(--vp-c-bg);
|
|
203
|
+
touch-action: none;
|
|
204
|
+
user-select: none;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.mindmap-canvas-host:deep(.mm-canvas),
|
|
208
|
+
.mindmap-canvas-host:deep(.mm-overlay) {
|
|
209
|
+
position: absolute;
|
|
210
|
+
inset: 0;
|
|
211
|
+
width: 100%;
|
|
212
|
+
height: 100%;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.mindmap-canvas-host:deep(.mm-overlay) { pointer-events: none; }
|
|
216
|
+
.mindmap-canvas-host:deep(.mm-editor) { outline: none; }
|
|
217
|
+
|
|
218
|
+
.mindmap-outline {
|
|
219
|
+
max-height: 560px;
|
|
220
|
+
padding: 18px 22px 22px;
|
|
221
|
+
overflow: auto;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.mindmap-outline-root,
|
|
225
|
+
.mindmap-outline :deep(ul) {
|
|
226
|
+
margin: 0;
|
|
227
|
+
padding: 0;
|
|
228
|
+
list-style: none;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.mindmap-outline :deep(.mindmap-outline-children) {
|
|
232
|
+
margin-left: 10px;
|
|
233
|
+
padding-left: 19px;
|
|
234
|
+
border-left: 1px solid var(--vp-c-divider);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.mindmap-outline :deep(.mindmap-outline-row) {
|
|
238
|
+
display: flex;
|
|
239
|
+
align-items: flex-start;
|
|
240
|
+
gap: 7px;
|
|
241
|
+
min-height: 30px;
|
|
242
|
+
padding: 3px 0;
|
|
243
|
+
color: var(--vp-c-text-1);
|
|
244
|
+
line-height: 24px;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.mindmap-outline :deep(.mindmap-outline-toggle),
|
|
248
|
+
.mindmap-outline :deep(.mindmap-outline-leaf) {
|
|
249
|
+
flex: 0 0 18px;
|
|
250
|
+
width: 18px;
|
|
251
|
+
color: var(--vp-c-text-3);
|
|
252
|
+
text-align: center;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.mindmap-outline :deep(.mindmap-outline-toggle:hover) { color: var(--vp-c-brand-1); }
|
|
256
|
+
.mindmap-outline :deep(.mindmap-outline-checkbox) { margin-top: 5px; }
|
|
257
|
+
.mindmap-outline :deep(.mindmap-outline-label) { min-width: 0; overflow-wrap: anywhere; }
|
|
258
|
+
.mindmap-outline :deep(.mindmap-outline-node.is-root > .mindmap-outline-row) { font-size: 18px; font-weight: 700; }
|
|
259
|
+
.mindmap-outline :deep(.mindmap-outline-node.is-done > .mindmap-outline-row .mindmap-outline-label) { opacity: .58; text-decoration: line-through; }
|
|
260
|
+
.mindmap-outline :deep(.mindmap-outline-image) { display: block; max-width: min(100%, 560px); max-height: 360px; margin: 5px 0 12px 25px; border-radius: 6px; }
|
|
261
|
+
.mindmap-outline :deep(.is-bold) { font-weight: 700; }
|
|
262
|
+
.mindmap-outline :deep(.is-italic) { font-style: italic; }
|
|
263
|
+
.mindmap-outline :deep(.is-underline) { text-decoration: underline; }
|
|
264
|
+
.mindmap-outline :deep(.is-strike) { text-decoration: line-through; }
|
|
265
|
+
.mindmap-outline :deep(.is-highlight) { padding: 0 2px; border-radius: 2px; background: #ffe56b; color: #252525; }
|
|
266
|
+
.mindmap-outline :deep(.is-code) { padding: 1px 5px; border-radius: 4px; background: var(--vp-c-bg-soft); color: var(--vp-c-danger-1); font-family: var(--vp-font-family-mono); }
|
|
267
|
+
.mindmap-outline :deep(.is-link) { color: var(--vp-c-brand-1); text-decoration: underline; text-underline-offset: 3px; }
|
|
268
|
+
|
|
269
|
+
.mindmap-source {
|
|
270
|
+
max-height: 560px;
|
|
271
|
+
margin: 0;
|
|
272
|
+
padding: 18px 22px;
|
|
273
|
+
overflow: auto;
|
|
274
|
+
border-radius: 0;
|
|
275
|
+
background: var(--vp-code-block-bg);
|
|
276
|
+
color: var(--vp-code-block-color);
|
|
277
|
+
font-size: 13px;
|
|
278
|
+
line-height: 1.65;
|
|
279
|
+
white-space: pre;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@media (max-width: 768px) {
|
|
283
|
+
.mindmap-canvas-host { height: 360px; }
|
|
284
|
+
.mindmap-preview-tabs button { padding-inline: 9px; }
|
|
285
|
+
.mindmap-outline { padding-inline: 12px; }
|
|
286
|
+
}
|
|
287
|
+
</style>
|