diff --git a/frontend/src/components/toolbar/BlockLanguageSelector.vue b/frontend/src/components/toolbar/BlockLanguageSelector.vue index a287727..13d5bca 100644 --- a/frontend/src/components/toolbar/BlockLanguageSelector.vue +++ b/frontend/src/components/toolbar/BlockLanguageSelector.vue @@ -8,7 +8,7 @@ import { getActiveNoteBlock } from '@/views/editor/extensions/codeblock/state'; import { changeCurrentBlockLanguage } from '@/views/editor/extensions/codeblock/commands'; const { t } = useI18n(); -const editorStore = readonly(useEditorStore()); +const editorStore = useEditorStore(); // 组件状态 const showLanguageMenu = shallowRef(false); diff --git a/frontend/src/components/toolbar/Toolbar.vue b/frontend/src/components/toolbar/Toolbar.vue index e6528ca..bc41b08 100644 --- a/frontend/src/components/toolbar/Toolbar.vue +++ b/frontend/src/components/toolbar/Toolbar.vue @@ -14,14 +14,13 @@ import {getLanguage} from '@/views/editor/extensions/codeblock/lang-parser/langu import {formatBlockContent} from '@/views/editor/extensions/codeblock/formatCode'; import {createDebounce} from '@/common/utils/debounce'; import {toggleMarkdownPreview} from '@/views/editor/extensions/markdownPreview'; -import {usePanelStore} from '@/stores/panelStore'; +import {markdownPreviewManager} from "@/views/editor/extensions/markdownPreview/manager"; -const editorStore = readonly(useEditorStore()); -const configStore = readonly(useConfigStore()); -const updateStore = readonly(useUpdateStore()); -const windowStore = readonly(useWindowStore()); -const systemStore = readonly(useSystemStore()); -const panelStore = readonly(usePanelStore()); +const editorStore = useEditorStore(); +const configStore = useConfigStore(); +const updateStore = useUpdateStore(); +const windowStore = useWindowStore(); +const systemStore = useSystemStore(); const {t} = useI18n(); const router = useRouter(); @@ -39,7 +38,7 @@ const isCurrentWindowOnTop = computed(() => { // 当前文档的预览是否打开 const isCurrentBlockPreviewing = computed(() => { - return panelStore.markdownPreview.isOpen && !panelStore.markdownPreview.isClosing; + return markdownPreviewManager.isVisible(); }); // 切换窗口置顶状态 diff --git a/frontend/src/stores/editorStore.ts b/frontend/src/stores/editorStore.ts index b98fcb6..52c60a5 100644 --- a/frontend/src/stores/editorStore.ts +++ b/frontend/src/stores/editorStore.ts @@ -4,7 +4,6 @@ import {EditorView} from '@codemirror/view'; import {EditorState, Extension} from '@codemirror/state'; import {useConfigStore} from './configStore'; import {useDocumentStore} from './documentStore'; -import {usePanelStore} from './panelStore'; import {ExtensionID} from '@/../bindings/voidraft/internal/models/models'; import {DocumentService, ExtensionService} from '@/../bindings/voidraft/internal/services'; import {ensureSyntaxTree} from "@codemirror/language"; @@ -30,7 +29,7 @@ import {generateContentHash} from "@/common/utils/hashUtils"; import {createTimerManager, type TimerManager} from '@/common/utils/timerUtils'; import {EDITOR_CONFIG} from '@/common/constant/editor'; import {createHttpClientExtension} from "@/views/editor/extensions/httpclient"; -import {markdownPreviewExtension, updateMarkdownPreviewTheme} from "@/views/editor/extensions/markdownPreview"; +import {markdownPreviewExtension} from "@/views/editor/extensions/markdownPreview"; import {createDebounce} from '@/common/utils/debounce'; export interface DocumentStats { @@ -642,12 +641,6 @@ export const useEditorStore = defineStore('editor', () => { }); }; - // 应用 Markdown 预览主题 - const applyPreviewThemeSettings = () => { - editorCache.values().forEach(instance => { - updateMarkdownPreviewTheme(instance.view); - }); - }; // 应用Tab设置 const applyTabSettings = () => { @@ -701,10 +694,6 @@ export const useEditorStore = defineStore('editor', () => { instance.view.destroy(); }); - // 清理 panelStore 状态(导航离开编辑器页面时) - const panelStore = usePanelStore(); - panelStore.reset(); - currentEditor.value = null; }; @@ -790,7 +779,6 @@ export const useEditorStore = defineStore('editor', () => { // 配置更新方法 applyFontSettings, applyThemeSettings, - applyPreviewThemeSettings, applyTabSettings, applyKeymapSettings, diff --git a/frontend/src/stores/panelStore.ts b/frontend/src/stores/panelStore.ts deleted file mode 100644 index 7df422e..0000000 --- a/frontend/src/stores/panelStore.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { defineStore } from 'pinia'; -import { ref, computed } from 'vue'; -import type { EditorView } from '@codemirror/view'; -import { useDocumentStore } from './documentStore'; - -/** - * 单个文档的预览状态 - */ -interface DocumentPreviewState { - isOpen: boolean; - isClosing: boolean; - blockFrom: number; - blockTo: number; -} - -/** - * 面板状态管理 Store - * 管理编辑器中各种面板的显示状态(按文档ID区分) - */ -export const usePanelStore = defineStore('panel', () => { - // 当前编辑器视图引用 - const editorView = ref(null); - - // 每个文档的预览状态 Map - const documentPreviews = ref>(new Map()); - - /** - * 获取当前文档的预览状态 - */ - const markdownPreview = computed(() => { - const documentStore = useDocumentStore(); - const currentDocId = documentStore.currentDocumentId; - - if (currentDocId === null) { - return { - isOpen: false, - isClosing: false, - blockFrom: 0, - blockTo: 0 - }; - } - - return documentPreviews.value.get(currentDocId) || { - isOpen: false, - isClosing: false, - blockFrom: 0, - blockTo: 0 - }; - }); - - /** - * 设置编辑器视图 - */ - const setEditorView = (view: EditorView | null) => { - editorView.value = view; - }; - - /** - * 打开 Markdown 预览面板 - */ - const openMarkdownPreview = (from: number, to: number) => { - const documentStore = useDocumentStore(); - const currentDocId = documentStore.currentDocumentId; - - if (currentDocId === null) return; - - documentPreviews.value.set(currentDocId, { - isOpen: true, - isClosing: false, - blockFrom: from, - blockTo: to - }); - }; - - /** - * 开始关闭 Markdown 预览面板 - */ - const startClosingMarkdownPreview = () => { - const documentStore = useDocumentStore(); - const currentDocId = documentStore.currentDocumentId; - - if (currentDocId === null) return; - - const state = documentPreviews.value.get(currentDocId); - if (state?.isOpen) { - documentPreviews.value.set(currentDocId, { - ...state, - isClosing: true - }); - } - }; - - /** - * 关闭 Markdown 预览面板 - */ - const closeMarkdownPreview = () => { - const documentStore = useDocumentStore(); - const currentDocId = documentStore.currentDocumentId; - - if (currentDocId === null) return; - - documentPreviews.value.set(currentDocId, { - isOpen: false, - isClosing: false, - blockFrom: 0, - blockTo: 0 - }); - }; - - /** - * 更新预览块的范围(用于实时预览) - */ - const updatePreviewRange = (from: number, to: number) => { - const documentStore = useDocumentStore(); - const currentDocId = documentStore.currentDocumentId; - - if (currentDocId === null) return; - - const state = documentPreviews.value.get(currentDocId); - if (state?.isOpen) { - documentPreviews.value.set(currentDocId, { - ...state, - blockFrom: from, - blockTo: to - }); - } - }; - - /** - * 检查指定块是否正在预览 - */ - const isBlockPreviewing = (from: number, to: number): boolean => { - const preview = markdownPreview.value; - return preview.isOpen && - preview.blockFrom === from && - preview.blockTo === to; - }; - - /** - * 重置所有面板状态 - */ - const reset = () => { - documentPreviews.value.clear(); - editorView.value = null; - }; - - /** - * 清理指定文档的预览状态(文档关闭时调用) - */ - const clearDocumentPreview = (documentId: number) => { - documentPreviews.value.delete(documentId); - }; - - return { - // 状态 - editorView, - markdownPreview, - - // 方法 - setEditorView, - openMarkdownPreview, - startClosingMarkdownPreview, - closeMarkdownPreview, - updatePreviewRange, - isBlockPreviewing, - reset, - clearDocumentPreview - }; -}); - diff --git a/frontend/src/stores/themeStore.ts b/frontend/src/stores/themeStore.ts index 31e57da..55787ef 100644 --- a/frontend/src/stores/themeStore.ts +++ b/frontend/src/stores/themeStore.ts @@ -141,7 +141,6 @@ export const useThemeStore = defineStore('theme', () => { const editorStore = useEditorStore(); editorStore?.applyThemeSettings(); - editorStore?.applyPreviewThemeSettings(); }; return { diff --git a/frontend/src/views/editor/Editor.vue b/frontend/src/views/editor/Editor.vue index 5fcfb40..adcdeaf 100644 --- a/frontend/src/views/editor/Editor.vue +++ b/frontend/src/views/editor/Editor.vue @@ -11,6 +11,8 @@ import ContextMenu from './contextMenu/ContextMenu.vue'; import { contextMenuManager } from './contextMenu/manager'; import TranslatorDialog from './extensions/translator/TranslatorDialog.vue'; import { translatorManager } from './extensions/translator/manager'; +import {markdownPreviewManager} from "@/views/editor/extensions/markdownPreview/manager"; +import PreviewPanel from "@/views/editor/extensions/markdownPreview/PreviewPanel.vue"; const editorStore = useEditorStore(); const documentStore = useDocumentStore(); @@ -37,6 +39,7 @@ onMounted(async () => { onBeforeUnmount(() => { contextMenuManager.destroy(); translatorManager.destroy(); + markdownPreviewManager.destroy(); }); @@ -46,8 +49,13 @@ onBeforeUnmount(() => { - -
+ +
+ +
+ + +
@@ -66,9 +74,18 @@ onBeforeUnmount(() => { flex-direction: column; position: relative; - .editor { + .editor-wrapper { width: 100%; flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + position: relative; + } + + .editor { + width: 100%; + height: 100%; overflow: hidden; position: relative; } diff --git a/frontend/src/views/editor/extensions/markdownPreview/PreviewPanel.vue b/frontend/src/views/editor/extensions/markdownPreview/PreviewPanel.vue new file mode 100644 index 0000000..c3f6b90 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdownPreview/PreviewPanel.vue @@ -0,0 +1,520 @@ + + + + + + diff --git a/frontend/src/views/editor/extensions/markdownPreview/github-markdown.css b/frontend/src/views/editor/extensions/markdownPreview/github-markdown.css new file mode 100644 index 0000000..d95cc85 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdownPreview/github-markdown.css @@ -0,0 +1,1229 @@ +.markdown-body { + --base-size-4: 0.25rem; + --base-size-8: 0.5rem; + --base-size-16: 1rem; + --base-size-24: 1.5rem; + --base-size-40: 2.5rem; + --base-text-weight-normal: 400; + --base-text-weight-medium: 500; + --base-text-weight-semibold: 600; + --fontStack-monospace: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; + --fgColor-accent: Highlight; +} + +/* 暗色主题变量 */ +.markdown-body[data-theme="dark"] { + /* dark */ + color-scheme: dark; + --focus-outlineColor: #1f6feb; + --fgColor-default: #f0f6fc; + --fgColor-muted: #9198a1; + --fgColor-accent: #4493f8; + --fgColor-success: #3fb950; + --fgColor-attention: #d29922; + --fgColor-danger: #f85149; + --fgColor-done: #ab7df8; + --bgColor-default: #0d1117; + --bgColor-muted: #151b23; + --bgColor-neutral-muted: #656c7633; + --bgColor-attention-muted: #bb800926; + --borderColor-default: #3d444d; + --borderColor-muted: #3d444db3; + --borderColor-neutral-muted: #3d444db3; + --borderColor-accent-emphasis: #1f6feb; + --borderColor-success-emphasis: #238636; + --borderColor-attention-emphasis: #9e6a03; + --borderColor-danger-emphasis: #da3633; + --borderColor-done-emphasis: #8957e5; + --color-prettylights-syntax-comment: #9198a1; + --color-prettylights-syntax-constant: #79c0ff; + --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; + --color-prettylights-syntax-entity: #d2a8ff; + --color-prettylights-syntax-storage-modifier-import: #f0f6fc; + --color-prettylights-syntax-entity-tag: #7ee787; + --color-prettylights-syntax-keyword: #ff7b72; + --color-prettylights-syntax-string: #a5d6ff; + --color-prettylights-syntax-variable: #ffa657; + --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; + --color-prettylights-syntax-brackethighlighter-angle: #9198a1; + --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; + --color-prettylights-syntax-invalid-illegal-bg: #8e1519; + --color-prettylights-syntax-carriage-return-text: #f0f6fc; + --color-prettylights-syntax-carriage-return-bg: #b62324; + --color-prettylights-syntax-string-regexp: #7ee787; + --color-prettylights-syntax-markup-list: #f2cc60; + --color-prettylights-syntax-markup-heading: #1f6feb; + --color-prettylights-syntax-markup-italic: #f0f6fc; + --color-prettylights-syntax-markup-bold: #f0f6fc; + --color-prettylights-syntax-markup-deleted-text: #ffdcd7; + --color-prettylights-syntax-markup-deleted-bg: #67060c; + --color-prettylights-syntax-markup-inserted-text: #aff5b4; + --color-prettylights-syntax-markup-inserted-bg: #033a16; + --color-prettylights-syntax-markup-changed-text: #ffdfb6; + --color-prettylights-syntax-markup-changed-bg: #5a1e02; + --color-prettylights-syntax-markup-ignored-text: #f0f6fc; + --color-prettylights-syntax-markup-ignored-bg: #1158c7; + --color-prettylights-syntax-meta-diff-range: #d2a8ff; + --color-prettylights-syntax-sublimelinter-gutter-mark: #3d444d; + } + +/* 亮色主题变量 */ +.markdown-body[data-theme="light"] { + /* light */ + color-scheme: light; + --focus-outlineColor: #0969da; + --fgColor-default: #1f2328; + --fgColor-muted: #59636e; + --fgColor-accent: #0969da; + --fgColor-success: #1a7f37; + --fgColor-attention: #9a6700; + --fgColor-danger: #d1242f; + --fgColor-done: #8250df; + --bgColor-default: #ffffff; + --bgColor-muted: #f6f8fa; + --bgColor-neutral-muted: #818b981f; + --bgColor-attention-muted: #fff8c5; + --borderColor-default: #d1d9e0; + --borderColor-muted: #d1d9e0b3; + --borderColor-neutral-muted: #d1d9e0b3; + --borderColor-accent-emphasis: #0969da; + --borderColor-success-emphasis: #1a7f37; + --borderColor-attention-emphasis: #9a6700; + --borderColor-danger-emphasis: #cf222e; + --borderColor-done-emphasis: #8250df; + --color-prettylights-syntax-comment: #59636e; + --color-prettylights-syntax-constant: #0550ae; + --color-prettylights-syntax-constant-other-reference-link: #0a3069; + --color-prettylights-syntax-entity: #6639ba; + --color-prettylights-syntax-storage-modifier-import: #1f2328; + --color-prettylights-syntax-entity-tag: #0550ae; + --color-prettylights-syntax-keyword: #cf222e; + --color-prettylights-syntax-string: #0a3069; + --color-prettylights-syntax-variable: #953800; + --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; + --color-prettylights-syntax-brackethighlighter-angle: #59636e; + --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; + --color-prettylights-syntax-invalid-illegal-bg: #82071e; + --color-prettylights-syntax-carriage-return-text: #f6f8fa; + --color-prettylights-syntax-carriage-return-bg: #cf222e; + --color-prettylights-syntax-string-regexp: #116329; + --color-prettylights-syntax-markup-list: #3b2300; + --color-prettylights-syntax-markup-heading: #0550ae; + --color-prettylights-syntax-markup-italic: #1f2328; + --color-prettylights-syntax-markup-bold: #1f2328; + --color-prettylights-syntax-markup-deleted-text: #82071e; + --color-prettylights-syntax-markup-deleted-bg: #ffebe9; + --color-prettylights-syntax-markup-inserted-text: #116329; + --color-prettylights-syntax-markup-inserted-bg: #dafbe1; + --color-prettylights-syntax-markup-changed-text: #953800; + --color-prettylights-syntax-markup-changed-bg: #ffd8b5; + --color-prettylights-syntax-markup-ignored-text: #d1d9e0; + --color-prettylights-syntax-markup-ignored-bg: #0550ae; + --color-prettylights-syntax-meta-diff-range: #8250df; + --color-prettylights-syntax-sublimelinter-gutter-mark: #818b98; + } + + +.markdown-body { + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + margin: 0; + color: var(--fgColor-default); + background-color: var(--bgColor-default); + font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; + font-size: 16px; + line-height: 1.5; + word-wrap: break-word; +} + +.markdown-body .octicon { + display: inline-block; + fill: currentColor; + vertical-align: text-bottom; +} + +.markdown-body h1:hover .anchor .octicon-link:before, +.markdown-body h2:hover .anchor .octicon-link:before, +.markdown-body h3:hover .anchor .octicon-link:before, +.markdown-body h4:hover .anchor .octicon-link:before, +.markdown-body h5:hover .anchor .octicon-link:before, +.markdown-body h6:hover .anchor .octicon-link:before { + width: 16px; + height: 16px; + content: ' '; + display: inline-block; + background-color: currentColor; + -webkit-mask-image: url("data:image/svg+xml,"); + mask-image: url("data:image/svg+xml,"); +} + +.markdown-body details, +.markdown-body figcaption, +.markdown-body figure { + display: block; +} + +.markdown-body summary { + display: list-item; +} + +.markdown-body [hidden] { + display: none !important; +} + +.markdown-body a { + background-color: transparent; + color: var(--fgColor-accent); + text-decoration: none; +} + +.markdown-body abbr[title] { + border-bottom: none; + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +.markdown-body b, +.markdown-body strong { + font-weight: var(--base-text-weight-semibold, 600); +} + +.markdown-body dfn { + font-style: italic; +} + +.markdown-body h1 { + margin: .67em 0; + font-weight: var(--base-text-weight-semibold, 600); + padding-bottom: .3em; + font-size: 2em; + border-bottom: 1px solid var(--borderColor-muted); +} + +.markdown-body mark { + background-color: var(--bgColor-attention-muted); + color: var(--fgColor-default); +} + +.markdown-body small { + font-size: 90%; +} + +.markdown-body sub, +.markdown-body sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +.markdown-body sub { + bottom: -0.25em; +} + +.markdown-body sup { + top: -0.5em; +} + +.markdown-body img { + border-style: none; + max-width: 100%; + box-sizing: content-box; +} + +.markdown-body code, +.markdown-body kbd, +.markdown-body pre, +.markdown-body samp { + font-family: monospace; + font-size: 1em; +} + +.markdown-body figure { + margin: 1em var(--base-size-40); +} + +.markdown-body hr { + box-sizing: content-box; + overflow: hidden; + background: transparent; + border-bottom: 1px solid var(--borderColor-muted); + height: .25em; + padding: 0; + margin: var(--base-size-24) 0; + background-color: var(--borderColor-default); + border: 0; +} + +.markdown-body input { + font: inherit; + margin: 0; + overflow: visible; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +.markdown-body [type=button], +.markdown-body [type=reset], +.markdown-body [type=submit] { + -webkit-appearance: button; + appearance: button; +} + +.markdown-body [type=checkbox], +.markdown-body [type=radio] { + box-sizing: border-box; + padding: 0; +} + +.markdown-body [type=number]::-webkit-inner-spin-button, +.markdown-body [type=number]::-webkit-outer-spin-button { + height: auto; +} + +.markdown-body [type=search]::-webkit-search-cancel-button, +.markdown-body [type=search]::-webkit-search-decoration { + -webkit-appearance: none; + appearance: none; +} + +.markdown-body ::-webkit-input-placeholder { + color: inherit; + opacity: .54; +} + +.markdown-body ::-webkit-file-upload-button { + -webkit-appearance: button; + appearance: button; + font: inherit; +} + +.markdown-body a:hover { + text-decoration: underline; +} + +.markdown-body ::placeholder { + color: var(--fgColor-muted); + opacity: 1; +} + +.markdown-body hr::before { + display: table; + content: ""; +} + +.markdown-body hr::after { + display: table; + clear: both; + content: ""; +} + +.markdown-body table { + border-spacing: 0; + border-collapse: collapse; + display: block; + width: max-content; + max-width: 100%; + overflow: auto; + font-variant: tabular-nums; +} + +.markdown-body td, +.markdown-body th { + padding: 0; +} + +.markdown-body details summary { + cursor: pointer; +} + +.markdown-body a:focus, +.markdown-body [role=button]:focus, +.markdown-body input[type=radio]:focus, +.markdown-body input[type=checkbox]:focus { + outline: 2px solid var(--focus-outlineColor); + outline-offset: -2px; + box-shadow: none; +} + +.markdown-body a:focus:not(:focus-visible), +.markdown-body [role=button]:focus:not(:focus-visible), +.markdown-body input[type=radio]:focus:not(:focus-visible), +.markdown-body input[type=checkbox]:focus:not(:focus-visible) { + outline: solid 1px transparent; +} + +.markdown-body a:focus-visible, +.markdown-body [role=button]:focus-visible, +.markdown-body input[type=radio]:focus-visible, +.markdown-body input[type=checkbox]:focus-visible { + outline: 2px solid var(--focus-outlineColor); + outline-offset: -2px; + box-shadow: none; +} + +.markdown-body a:not([class]):focus, +.markdown-body a:not([class]):focus-visible, +.markdown-body input[type=radio]:focus, +.markdown-body input[type=radio]:focus-visible, +.markdown-body input[type=checkbox]:focus, +.markdown-body input[type=checkbox]:focus-visible { + outline-offset: 0; +} + +.markdown-body kbd { + display: inline-block; + padding: var(--base-size-4); + font: 11px var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace); + line-height: 10px; + color: var(--fgColor-default); + vertical-align: middle; + background-color: var(--bgColor-muted); + border: solid 1px var(--borderColor-neutral-muted); + border-bottom-color: var(--borderColor-neutral-muted); + border-radius: 6px; + box-shadow: inset 0 -1px 0 var(--borderColor-neutral-muted); +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: var(--base-size-24); + margin-bottom: var(--base-size-16); + font-weight: var(--base-text-weight-semibold, 600); + line-height: 1.25; +} + +.markdown-body h2 { + font-weight: var(--base-text-weight-semibold, 600); + padding-bottom: .3em; + font-size: 1.5em; + border-bottom: 1px solid var(--borderColor-muted); +} + +.markdown-body h3 { + font-weight: var(--base-text-weight-semibold, 600); + font-size: 1.25em; +} + +.markdown-body h4 { + font-weight: var(--base-text-weight-semibold, 600); + font-size: 1em; +} + +.markdown-body h5 { + font-weight: var(--base-text-weight-semibold, 600); + font-size: .875em; +} + +.markdown-body h6 { + font-weight: var(--base-text-weight-semibold, 600); + font-size: .85em; + color: var(--fgColor-muted); +} + +.markdown-body p { + margin-top: 0; + margin-bottom: 10px; +} + +.markdown-body blockquote { + margin: 0; + padding: 0 1em; + color: var(--fgColor-muted); + border-left: .25em solid var(--borderColor-default); +} + +.markdown-body ul, +.markdown-body ol { + margin-top: 0; + margin-bottom: 0; + padding-left: 2em; +} + +.markdown-body ol ol, +.markdown-body ul ol { + list-style-type: lower-roman; +} + +.markdown-body ul ul ol, +.markdown-body ul ol ol, +.markdown-body ol ul ol, +.markdown-body ol ol ol { + list-style-type: lower-alpha; +} + +.markdown-body dd { + margin-left: 0; +} + +.markdown-body tt, +.markdown-body code, +.markdown-body samp { + font-family: var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace); + font-size: 12px; +} + +.markdown-body pre { + margin-top: 0; + margin-bottom: 0; + font-family: var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace); + font-size: 12px; + word-wrap: normal; +} + +.markdown-body .octicon { + display: inline-block; + overflow: visible !important; + vertical-align: text-bottom; + fill: currentColor; +} + +.markdown-body input::-webkit-outer-spin-button, +.markdown-body input::-webkit-inner-spin-button { + margin: 0; + appearance: none; +} + +.markdown-body .mr-2 { + margin-right: var(--base-size-8, 8px) !important; +} + +.markdown-body::before { + display: table; + content: ""; +} + +.markdown-body::after { + display: table; + clear: both; + content: ""; +} + +.markdown-body>*:first-child { + margin-top: 0 !important; +} + +.markdown-body>*:last-child { + margin-bottom: 0 !important; +} + +.markdown-body a:not([href]) { + color: inherit; + text-decoration: none; +} + +.markdown-body .absent { + color: var(--fgColor-danger); +} + +.markdown-body .anchor { + float: left; + padding-right: var(--base-size-4); + margin-left: -20px; + line-height: 1; +} + +.markdown-body .anchor:focus { + outline: none; +} + +.markdown-body p, +.markdown-body blockquote, +.markdown-body ul, +.markdown-body ol, +.markdown-body dl, +.markdown-body table, +.markdown-body pre, +.markdown-body details { + margin-top: 0; + margin-bottom: var(--base-size-16); +} + +.markdown-body blockquote>:first-child { + margin-top: 0; +} + +.markdown-body blockquote>:last-child { + margin-bottom: 0; +} + +.markdown-body h1 .octicon-link, +.markdown-body h2 .octicon-link, +.markdown-body h3 .octicon-link, +.markdown-body h4 .octicon-link, +.markdown-body h5 .octicon-link, +.markdown-body h6 .octicon-link { + color: var(--fgColor-default); + vertical-align: middle; + visibility: hidden; +} + +.markdown-body h1:hover .anchor, +.markdown-body h2:hover .anchor, +.markdown-body h3:hover .anchor, +.markdown-body h4:hover .anchor, +.markdown-body h5:hover .anchor, +.markdown-body h6:hover .anchor { + text-decoration: none; +} + +.markdown-body h1:hover .anchor .octicon-link, +.markdown-body h2:hover .anchor .octicon-link, +.markdown-body h3:hover .anchor .octicon-link, +.markdown-body h4:hover .anchor .octicon-link, +.markdown-body h5:hover .anchor .octicon-link, +.markdown-body h6:hover .anchor .octicon-link { + visibility: visible; +} + +.markdown-body h1 tt, +.markdown-body h1 code, +.markdown-body h2 tt, +.markdown-body h2 code, +.markdown-body h3 tt, +.markdown-body h3 code, +.markdown-body h4 tt, +.markdown-body h4 code, +.markdown-body h5 tt, +.markdown-body h5 code, +.markdown-body h6 tt, +.markdown-body h6 code { + padding: 0 .2em; + font-size: inherit; +} + +.markdown-body summary h1, +.markdown-body summary h2, +.markdown-body summary h3, +.markdown-body summary h4, +.markdown-body summary h5, +.markdown-body summary h6 { + display: inline-block; +} + +.markdown-body summary h1 .anchor, +.markdown-body summary h2 .anchor, +.markdown-body summary h3 .anchor, +.markdown-body summary h4 .anchor, +.markdown-body summary h5 .anchor, +.markdown-body summary h6 .anchor { + margin-left: -40px; +} + +.markdown-body summary h1, +.markdown-body summary h2 { + padding-bottom: 0; + border-bottom: 0; +} + +.markdown-body ul.no-list, +.markdown-body ol.no-list { + padding: 0; + list-style-type: none; +} + +.markdown-body ol[type="a s"] { + list-style-type: lower-alpha; +} + +.markdown-body ol[type="A s"] { + list-style-type: upper-alpha; +} + +.markdown-body ol[type="i s"] { + list-style-type: lower-roman; +} + +.markdown-body ol[type="I s"] { + list-style-type: upper-roman; +} + +.markdown-body ol[type="1"] { + list-style-type: decimal; +} + +.markdown-body div>ol:not([type]) { + list-style-type: decimal; +} + +.markdown-body ul ul, +.markdown-body ul ol, +.markdown-body ol ol, +.markdown-body ol ul { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body li>p { + margin-top: var(--base-size-16); +} + +.markdown-body li+li { + margin-top: .25em; +} + +.markdown-body dl { + padding: 0; +} + +.markdown-body dl dt { + padding: 0; + margin-top: var(--base-size-16); + font-size: 1em; + font-style: italic; + font-weight: var(--base-text-weight-semibold, 600); +} + +.markdown-body dl dd { + padding: 0 var(--base-size-16); + margin-bottom: var(--base-size-16); +} + +.markdown-body table th { + font-weight: var(--base-text-weight-semibold, 600); +} + +.markdown-body table th, +.markdown-body table td { + padding: 6px 13px; + border: 1px solid var(--borderColor-default); +} + +.markdown-body table td>:last-child { + margin-bottom: 0; +} + +.markdown-body table tr { + background-color: var(--bgColor-default); + border-top: 1px solid var(--borderColor-muted); +} + +.markdown-body table tr:nth-child(2n) { + background-color: var(--bgColor-muted); +} + +.markdown-body table img { + background-color: transparent; +} + +.markdown-body img[align=right] { + padding-left: 20px; +} + +.markdown-body img[align=left] { + padding-right: 20px; +} + +.markdown-body .emoji { + max-width: none; + vertical-align: text-top; + background-color: transparent; +} + +.markdown-body span.frame { + display: block; + overflow: hidden; +} + +.markdown-body span.frame>span { + display: block; + float: left; + width: auto; + padding: 7px; + margin: 13px 0 0; + overflow: hidden; + border: 1px solid var(--borderColor-default); +} + +.markdown-body span.frame span img { + display: block; + float: left; +} + +.markdown-body span.frame span span { + display: block; + padding: 5px 0 0; + clear: both; + color: var(--fgColor-default); +} + +.markdown-body span.align-center { + display: block; + overflow: hidden; + clear: both; +} + +.markdown-body span.align-center>span { + display: block; + margin: 13px auto 0; + overflow: hidden; + text-align: center; +} + +.markdown-body span.align-center span img { + margin: 0 auto; + text-align: center; +} + +.markdown-body span.align-right { + display: block; + overflow: hidden; + clear: both; +} + +.markdown-body span.align-right>span { + display: block; + margin: 13px 0 0; + overflow: hidden; + text-align: right; +} + +.markdown-body span.align-right span img { + margin: 0; + text-align: right; +} + +.markdown-body span.float-left { + display: block; + float: left; + margin-right: 13px; + overflow: hidden; +} + +.markdown-body span.float-left span { + margin: 13px 0 0; +} + +.markdown-body span.float-right { + display: block; + float: right; + margin-left: 13px; + overflow: hidden; +} + +.markdown-body span.float-right>span { + display: block; + margin: 13px auto 0; + overflow: hidden; + text-align: right; +} + +.markdown-body code, +.markdown-body tt { + padding: .2em .4em; + margin: 0; + font-size: 85%; + white-space: break-spaces; + background-color: var(--bgColor-neutral-muted); + border-radius: 6px; +} + +.markdown-body code br, +.markdown-body tt br { + display: none; +} + +.markdown-body del code { + text-decoration: inherit; +} + +.markdown-body samp { + font-size: 85%; +} + +.markdown-body pre code { + font-size: 100%; +} + +.markdown-body pre>code { + padding: 0; + margin: 0; + word-break: normal; + white-space: pre; + background: transparent; + border: 0; +} + +.markdown-body .highlight { + margin-bottom: var(--base-size-16); +} + +.markdown-body .highlight pre { + margin-bottom: 0; + word-break: normal; +} + +.markdown-body .highlight pre, +.markdown-body pre { + padding: var(--base-size-16); + overflow: auto; + font-size: 85%; + line-height: 1.45; + color: var(--fgColor-default); + background-color: var(--bgColor-muted); + border-radius: 6px; +} + +.markdown-body pre code, +.markdown-body pre tt { + display: inline; + max-width: fit-content; + padding: 0; + margin: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal; + background-color: transparent; + border: 0; +} + +.markdown-body .csv-data td, +.markdown-body .csv-data th { + padding: 5px; + overflow: hidden; + font-size: 12px; + line-height: 1; + text-align: left; + white-space: nowrap; +} + +.markdown-body .csv-data .blob-num { + padding: 10px var(--base-size-8) 9px; + text-align: right; + background: var(--bgColor-default); + border: 0; +} + +.markdown-body .csv-data tr { + border-top: 0; +} + +.markdown-body .csv-data th { + font-weight: var(--base-text-weight-semibold, 600); + background: var(--bgColor-muted); + border-top: 0; +} + +.markdown-body [data-footnote-ref]::before { + content: "["; +} + +.markdown-body [data-footnote-ref]::after { + content: "]"; +} + +.markdown-body .footnotes { + font-size: 12px; + color: var(--fgColor-muted); + border-top: 1px solid var(--borderColor-default); +} + +.markdown-body .footnotes ol { + padding-left: var(--base-size-16); +} + +.markdown-body .footnotes ol ul { + display: inline-block; + padding-left: var(--base-size-16); + margin-top: var(--base-size-16); +} + +.markdown-body .footnotes li { + position: relative; +} + +.markdown-body .footnotes li:target::before { + position: absolute; + top: calc(var(--base-size-8)*-1); + right: calc(var(--base-size-8)*-1); + bottom: calc(var(--base-size-8)*-1); + left: calc(var(--base-size-24)*-1); + pointer-events: none; + content: ""; + border: 2px solid var(--borderColor-accent-emphasis); + border-radius: 6px; +} + +.markdown-body .footnotes li:target { + color: var(--fgColor-default); +} + +.markdown-body .footnotes .data-footnote-backref g-emoji { + font-family: monospace; +} + +.markdown-body body:has(:modal) { + padding-right: var(--dialog-scrollgutter) !important; +} + +.markdown-body .pl-c { + color: var(--color-prettylights-syntax-comment); +} + +.markdown-body .pl-c1, +.markdown-body .pl-s .pl-v { + color: var(--color-prettylights-syntax-constant); +} + +.markdown-body .pl-e, +.markdown-body .pl-en { + color: var(--color-prettylights-syntax-entity); +} + +.markdown-body .pl-smi, +.markdown-body .pl-s .pl-s1 { + color: var(--color-prettylights-syntax-storage-modifier-import); +} + +.markdown-body .pl-ent { + color: var(--color-prettylights-syntax-entity-tag); +} + +.markdown-body .pl-k { + color: var(--color-prettylights-syntax-keyword); +} + +.markdown-body .pl-s, +.markdown-body .pl-pds, +.markdown-body .pl-s .pl-pse .pl-s1, +.markdown-body .pl-sr, +.markdown-body .pl-sr .pl-cce, +.markdown-body .pl-sr .pl-sre, +.markdown-body .pl-sr .pl-sra { + color: var(--color-prettylights-syntax-string); +} + +.markdown-body .pl-v, +.markdown-body .pl-smw { + color: var(--color-prettylights-syntax-variable); +} + +.markdown-body .pl-bu { + color: var(--color-prettylights-syntax-brackethighlighter-unmatched); +} + +.markdown-body .pl-ii { + color: var(--color-prettylights-syntax-invalid-illegal-text); + background-color: var(--color-prettylights-syntax-invalid-illegal-bg); +} + +.markdown-body .pl-c2 { + color: var(--color-prettylights-syntax-carriage-return-text); + background-color: var(--color-prettylights-syntax-carriage-return-bg); +} + +.markdown-body .pl-sr .pl-cce { + font-weight: bold; + color: var(--color-prettylights-syntax-string-regexp); +} + +.markdown-body .pl-ml { + color: var(--color-prettylights-syntax-markup-list); +} + +.markdown-body .pl-mh, +.markdown-body .pl-mh .pl-en, +.markdown-body .pl-ms { + font-weight: bold; + color: var(--color-prettylights-syntax-markup-heading); +} + +.markdown-body .pl-mi { + font-style: italic; + color: var(--color-prettylights-syntax-markup-italic); +} + +.markdown-body .pl-mb { + font-weight: bold; + color: var(--color-prettylights-syntax-markup-bold); +} + +.markdown-body .pl-md { + color: var(--color-prettylights-syntax-markup-deleted-text); + background-color: var(--color-prettylights-syntax-markup-deleted-bg); +} + +.markdown-body .pl-mi1 { + color: var(--color-prettylights-syntax-markup-inserted-text); + background-color: var(--color-prettylights-syntax-markup-inserted-bg); +} + +.markdown-body .pl-mc { + color: var(--color-prettylights-syntax-markup-changed-text); + background-color: var(--color-prettylights-syntax-markup-changed-bg); +} + +.markdown-body .pl-mi2 { + color: var(--color-prettylights-syntax-markup-ignored-text); + background-color: var(--color-prettylights-syntax-markup-ignored-bg); +} + +.markdown-body .pl-mdr { + font-weight: bold; + color: var(--color-prettylights-syntax-meta-diff-range); +} + +.markdown-body .pl-ba { + color: var(--color-prettylights-syntax-brackethighlighter-angle); +} + +.markdown-body .pl-sg { + color: var(--color-prettylights-syntax-sublimelinter-gutter-mark); +} + +.markdown-body .pl-corl { + text-decoration: underline; + color: var(--color-prettylights-syntax-constant-other-reference-link); +} + +.markdown-body [role=button]:focus:not(:focus-visible), +.markdown-body [role=tabpanel][tabindex="0"]:focus:not(:focus-visible), +.markdown-body button:focus:not(:focus-visible), +.markdown-body summary:focus:not(:focus-visible), +.markdown-body a:focus:not(:focus-visible) { + outline: none; + box-shadow: none; +} + +.markdown-body [tabindex="0"]:focus:not(:focus-visible), +.markdown-body details-dialog:focus:not(:focus-visible) { + outline: none; +} + +.markdown-body g-emoji { + display: inline-block; + min-width: 1ch; + font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; + font-size: 1em; + font-style: normal !important; + font-weight: var(--base-text-weight-normal, 400); + line-height: 1; + vertical-align: -0.075em; +} + +.markdown-body g-emoji img { + width: 1em; + height: 1em; +} + +.markdown-body .task-list-item { + list-style-type: none; +} + +.markdown-body .task-list-item label { + font-weight: var(--base-text-weight-normal, 400); +} + +.markdown-body .task-list-item.enabled label { + cursor: pointer; +} + +.markdown-body .task-list-item+.task-list-item { + margin-top: var(--base-size-4); +} + +.markdown-body .task-list-item .handle { + display: none; +} + +.markdown-body .task-list-item-checkbox { + margin: 0 .2em .25em -1.4em; + vertical-align: middle; +} + +.markdown-body ul:dir(rtl) .task-list-item-checkbox { + margin: 0 -1.6em .25em .2em; +} + +.markdown-body ol:dir(rtl) .task-list-item-checkbox { + margin: 0 -1.6em .25em .2em; +} + +.markdown-body .contains-task-list:hover .task-list-item-convert-container, +.markdown-body .contains-task-list:focus-within .task-list-item-convert-container { + display: block; + width: auto; + height: 24px; + overflow: visible; + clip: auto; +} + +.markdown-body ::-webkit-calendar-picker-indicator { + filter: invert(50%); +} + +.markdown-body .markdown-alert { + padding: var(--base-size-8) var(--base-size-16); + margin-bottom: var(--base-size-16); + color: inherit; + border-left: .25em solid var(--borderColor-default); +} + +.markdown-body .markdown-alert>:first-child { + margin-top: 0; +} + +.markdown-body .markdown-alert>:last-child { + margin-bottom: 0; +} + +.markdown-body .markdown-alert .markdown-alert-title { + display: flex; + font-weight: var(--base-text-weight-medium, 500); + align-items: center; + line-height: 1; +} + +.markdown-body .markdown-alert.markdown-alert-note { + border-left-color: var(--borderColor-accent-emphasis); +} + +.markdown-body .markdown-alert.markdown-alert-note .markdown-alert-title { + color: var(--fgColor-accent); +} + +.markdown-body .markdown-alert.markdown-alert-important { + border-left-color: var(--borderColor-done-emphasis); +} + +.markdown-body .markdown-alert.markdown-alert-important .markdown-alert-title { + color: var(--fgColor-done); +} + +.markdown-body .markdown-alert.markdown-alert-warning { + border-left-color: var(--borderColor-attention-emphasis); +} + +.markdown-body .markdown-alert.markdown-alert-warning .markdown-alert-title { + color: var(--fgColor-attention); +} + +.markdown-body .markdown-alert.markdown-alert-tip { + border-left-color: var(--borderColor-success-emphasis); +} + +.markdown-body .markdown-alert.markdown-alert-tip .markdown-alert-title { + color: var(--fgColor-success); +} + +.markdown-body .markdown-alert.markdown-alert-caution { + border-left-color: var(--borderColor-danger-emphasis); +} + +.markdown-body .markdown-alert.markdown-alert-caution .markdown-alert-title { + color: var(--fgColor-danger); +} + +.markdown-body>*:first-child>.heading-element:first-child { + margin-top: 0 !important; +} + +.markdown-body .highlight pre:has(+.zeroclipboard-container) { + min-height: 52px; +} + diff --git a/frontend/src/views/editor/extensions/markdownPreview/index.ts b/frontend/src/views/editor/extensions/markdownPreview/index.ts index a927001..224a2c4 100644 --- a/frontend/src/views/editor/extensions/markdownPreview/index.ts +++ b/frontend/src/views/editor/extensions/markdownPreview/index.ts @@ -1,22 +1,14 @@ -/** - * Markdown 预览扩展主入口 - */ -import { EditorView } from "@codemirror/view"; -import { Compartment } from "@codemirror/state"; -import { useThemeStore } from "@/stores/themeStore"; -import { usePanelStore } from "@/stores/panelStore"; +import { EditorView, ViewUpdate, ViewPlugin } from "@codemirror/view"; +import { Extension } from "@codemirror/state"; import { useDocumentStore } from "@/stores/documentStore"; import { getActiveNoteBlock } from "../codeblock/state"; -import { createMarkdownPreviewTheme } from "./styles"; -import { previewPanelState, previewPanelPlugin, togglePreview, closePreviewWithAnimation } from "./state"; +import { markdownPreviewManager } from "./manager"; /** * 切换预览面板的命令 */ export function toggleMarkdownPreview(view: EditorView): boolean { - const panelStore = usePanelStore(); const documentStore = useDocumentStore(); - const currentState = view.state.field(previewPanelState, false); const activeBlock = getActiveNoteBlock(view.state as any); // 如果当前没有激活的 Markdown 块,不执行操作 @@ -30,53 +22,84 @@ export function toggleMarkdownPreview(view: EditorView): boolean { return false; } - // 如果预览面板已打开(无论预览的是不是当前块),关闭预览 - if (panelStore.markdownPreview.isOpen && !panelStore.markdownPreview.isClosing) { - // 使用带动画的关闭函数 - closePreviewWithAnimation(view); + // 切换预览状态 + if (markdownPreviewManager.isVisible()) { + markdownPreviewManager.hide(); } else { - // 否则,打开当前块的预览 - view.dispatch({ - effects: togglePreview.of({ - documentId: currentDocumentId, - blockFrom: activeBlock.content.from, - blockTo: activeBlock.content.to - }) - }); - - // 注意:store 状态由 ViewPlugin 在面板创建成功后更新 + markdownPreviewManager.show( + view, + currentDocumentId, + activeBlock.content.from, + activeBlock.content.to + ); } return true; } +/** + * 预览同步插件 + */ +const previewSyncPlugin = ViewPlugin.fromClass( + class { + constructor(private view: EditorView) {} + + update(update: ViewUpdate) { + // 只在预览可见时处理 + if (!markdownPreviewManager.isVisible()) { + return; + } + + const documentStore = useDocumentStore(); + const currentDocumentId = documentStore.currentDocumentId; + const previewDocId = markdownPreviewManager.getCurrentDocumentId(); + + // 如果切换了文档,关闭预览 + if (currentDocumentId !== previewDocId) { + markdownPreviewManager.hide(); + return; + } + + // 文档内容改变时,更新预览 + if (update.docChanged) { + const activeBlock = getActiveNoteBlock(update.state as any); + + // 如果不再是 Markdown 块,关闭预览 + if (!activeBlock || activeBlock.language.name.toLowerCase() !== 'md') { + markdownPreviewManager.hide(); + return; + } + + const range = markdownPreviewManager.getCurrentBlockRange(); + + // 如果切换到其他块,关闭预览 + if (range && activeBlock.content.from !== range.from) { + markdownPreviewManager.hide(); + return; + } + + // 更新预览内容 + const newContent = update.state.doc.sliceString( + activeBlock.content.from, + activeBlock.content.to + ); + markdownPreviewManager.updateContent( + newContent, + activeBlock.content.from, + activeBlock.content.to + ); + } + } + + destroy() { + markdownPreviewManager.destroy(); + } + } +); + /** * 导出 Markdown 预览扩展 */ -const previewThemeCompartment = new Compartment(); - -const buildPreviewTheme = () => { - const themeStore = useThemeStore(); - const colors = themeStore.currentColors; - return colors ? createMarkdownPreviewTheme(colors) : EditorView.baseTheme({}); -}; - -export function markdownPreviewExtension() { - return [ - previewPanelState, - previewPanelPlugin, - previewThemeCompartment.of(buildPreviewTheme()) - ]; -} - -export function updateMarkdownPreviewTheme(view: EditorView): void { - if (!view?.dispatch) return; - - try { - view.dispatch({ - effects: previewThemeCompartment.reconfigure(buildPreviewTheme()) - }); - } catch (error) { - console.error("Failed to update markdown preview theme", error); - } +export function markdownPreviewExtension(): Extension { + return [previewSyncPlugin]; } diff --git a/frontend/src/views/editor/extensions/markdownPreview/manager.ts b/frontend/src/views/editor/extensions/markdownPreview/manager.ts new file mode 100644 index 0000000..8b60a1e --- /dev/null +++ b/frontend/src/views/editor/extensions/markdownPreview/manager.ts @@ -0,0 +1,161 @@ +import type { EditorView } from '@codemirror/view'; +import { shallowRef, type ShallowRef } from 'vue'; + +/** + * 预览面板位置配置 + */ +interface PreviewPosition { + height: number; +} + +/** + * 预览状态 + */ +interface PreviewState { + visible: boolean; + position: PreviewPosition; + content: string; + blockFrom: number; + blockTo: number; + documentId: number | null; + view: EditorView | null; +} + +/** + * Markdown 预览管理器类 + */ +class MarkdownPreviewManager { + private state: ShallowRef = shallowRef({ + visible: false, + position: { height: 300 }, + content: '', + blockFrom: 0, + blockTo: 0, + documentId: null, + view: null + }); + + /** + * 获取状态(供 Vue 组件使用) + */ + useState() { + return this.state; + } + + /** + * 显示预览面板 + */ + show(view: EditorView, documentId: number, blockFrom: number, blockTo: number): void { + const content = view.state.doc.sliceString(blockFrom, blockTo); + + // 计算初始高度(编辑器容器高度的 50%) + const containerHeight = view.dom.parentElement?.clientHeight || view.dom.clientHeight; + const defaultHeight = Math.floor(containerHeight * 0.5); + + this.state.value = { + visible: true, + position: { height: Math.max(100, defaultHeight) }, + content, + blockFrom, + blockTo, + documentId, + view + }; + } + + /** + * 更新预览内容(文档编辑时调用) + */ + updateContent(content: string, blockFrom: number, blockTo: number): void { + if (!this.state.value.visible) return; + + this.state.value = { + ...this.state.value, + content, + blockFrom, + blockTo + }; + } + + /** + * 更新面板高度 + */ + updateHeight(height: number): void { + if (!this.state.value.visible) return; + + this.state.value = { + ...this.state.value, + position: { height: Math.max(100, height) } + }; + } + + /** + * 隐藏预览面板 + */ + hide(): void { + if (!this.state.value.visible) return; + + const view = this.state.value.view; + + this.state.value = { + visible: false, + position: { height: 300 }, + content: '', + blockFrom: 0, + blockTo: 0, + documentId: null, + view: null + }; + + // 关闭后聚焦编辑器 + if (view) { + view.focus(); + } + } + + /** + * 检查预览是否可见 + */ + isVisible(): boolean { + return this.state.value.visible; + } + + /** + * 获取当前预览的文档 ID + */ + getCurrentDocumentId(): number | null { + return this.state.value.documentId; + } + + /** + * 获取当前预览的块范围 + */ + getCurrentBlockRange(): { from: number; to: number } | null { + if (!this.state.value.visible) return null; + return { + from: this.state.value.blockFrom, + to: this.state.value.blockTo + }; + } + + /** + * 清理资源 + */ + destroy(): void { + this.state.value = { + visible: false, + position: { height: 300 }, + content: '', + blockFrom: 0, + blockTo: 0, + documentId: null, + view: null + }; + } +} + +/** + * 导出单例 + */ +export const markdownPreviewManager = new MarkdownPreviewManager(); + diff --git a/frontend/src/views/editor/extensions/markdownPreview/panel.ts b/frontend/src/views/editor/extensions/markdownPreview/panel.ts deleted file mode 100644 index eaa5c57..0000000 --- a/frontend/src/views/editor/extensions/markdownPreview/panel.ts +++ /dev/null @@ -1,393 +0,0 @@ -/** - * Markdown 预览面板 UI 组件 - */ -import {EditorView, Panel, ViewUpdate} from "@codemirror/view"; -import MarkdownIt from 'markdown-it'; -import * as runtime from "@wailsio/runtime"; -import {previewPanelState} from "./state"; -import {createMarkdownRenderer} from "./markdownRenderer"; -import {updateMermaidTheme} from "@/common/markdown-it/plugins/markdown-it-mermaid"; -import {useThemeStore} from "@/stores/themeStore"; -import {usePanelStore} from "@/stores/panelStore"; -import {watch} from "vue"; -import {createDebounce} from "@/common/utils/debounce"; -import {morphHTML} from "@/common/utils/domDiff"; - -/** - * Markdown 预览面板类 - */ -export class MarkdownPreviewPanel { - private md: MarkdownIt; - private readonly dom: HTMLDivElement; - private readonly resizeHandle: HTMLDivElement; - private readonly content: HTMLDivElement; - private view: EditorView; - private themeUnwatchers: Array<() => void> = []; - private lastRenderedContent: string = ""; - private readonly debouncedUpdate: ReturnType; - private isDestroyed: boolean = false; // 标记面板是否已销毁 - - constructor(view: EditorView) { - this.view = view; - this.md = createMarkdownRenderer(); - - // 创建防抖更新函数 - this.debouncedUpdate = createDebounce(() => { - this.updateContentInternal(); - }, { delay: 500 }); - - // 监听主题变化 - const themeStore = useThemeStore(); - this.themeUnwatchers.push( - watch(() => themeStore.isDarkMode, (isDark) => { - const newTheme = isDark ? "dark" : "default"; - updateMermaidTheme(newTheme); - this.resetPreviewContent(); - }) - ); - this.themeUnwatchers.push( - watch( - () => themeStore.currentColors, - () => { - this.resetPreviewContent(); - }, - { deep: true } - ) - ); - - // 创建 DOM 结构 - this.dom = document.createElement("div"); - this.dom.className = "cm-markdown-preview-panel"; - - this.resizeHandle = document.createElement("div"); - this.resizeHandle.className = "cm-preview-resize-handle"; - - this.content = document.createElement("div"); - this.content.className = "cm-preview-content"; - - this.dom.appendChild(this.resizeHandle); - this.dom.appendChild(this.content); - - // 设置默认高度为编辑器高度的一半 - const defaultHeight = Math.floor(this.view.dom.clientHeight / 2); - this.dom.style.height = `${defaultHeight}px`; - - // 初始化拖动功能 - this.initResize(); - - // 初始化链接点击处理 - this.initLinkHandler(); - - // 初始渲染 - this.updateContentInternal(); - } - - /** - * 初始化链接点击处理(事件委托) - */ - private initLinkHandler(): void { - this.content.addEventListener('click', (e) => { - const target = e.target as HTMLElement; - - // 查找最近的 标签 - let linkElement = target; - while (linkElement && linkElement !== this.content) { - if (linkElement.tagName === 'A') { - const anchor = linkElement as HTMLAnchorElement; - const href = anchor.getAttribute('href'); - - // 处理脚注内部锚点链接 - if (href && href.startsWith('#')) { - e.preventDefault(); - - // 在预览面板内查找目标元素 - const targetId = href.substring(1); - - // 使用 getElementById 而不是 querySelector,因为 ID 可能包含特殊字符(如冒号) - const targetElement = document.getElementById(targetId); - - if (targetElement && this.content.contains(targetElement)) { - // 平滑滚动到目标元素 - targetElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - } - return; - } - - // 处理带 data-href 的外部链接 - if (anchor.hasAttribute('data-href')) { - e.preventDefault(); - const url = anchor.getAttribute('data-href'); - if (url && this.isValidUrl(url)) { - runtime.Browser.OpenURL(url); - } - return; - } - - // 处理其他链接 - if (href && !href.startsWith('#')) { - e.preventDefault(); - - // 只有有效的 URL(http/https/mailto/file 等)才用浏览器打开 - if (this.isValidUrl(href)) { - runtime.Browser.OpenURL(href); - } else { - // 相对路径或无效链接,显示提示 - console.warn('Invalid or relative link in preview:', href); - } - return; - } - } - - linkElement = linkElement.parentElement as HTMLElement; - } - }); - } - - /** - * 检查是否是有效的 URL(包含协议) - */ - private isValidUrl(url: string): boolean { - try { - // 检查是否包含协议 - if (url.match(/^[a-zA-Z][a-zA-Z\d+\-.]*:/)) { - const parsedUrl = new URL(url); - // 允许的协议列表 - const allowedProtocols = ['http:', 'https:', 'mailto:', 'file:', 'ftp:']; - return allowedProtocols.includes(parsedUrl.protocol); - } - return false; - } catch { - return false; - } - } - - /** - * 初始化拖动调整高度功能 - */ - private initResize(): void { - let startY = 0; - let startHeight = 0; - - const onMouseMove = (e: MouseEvent) => { - const delta = startY - e.clientY; - const maxHeight = this.getMaxHeight(); - const newHeight = Math.max(100, Math.min(maxHeight, startHeight + delta)); - this.dom.style.height = `${newHeight}px`; - }; - - const onMouseUp = () => { - document.removeEventListener("mousemove", onMouseMove); - document.removeEventListener("mouseup", onMouseUp); - this.resizeHandle.classList.remove("dragging"); - // 恢复 body 样式 - document.body.style.cursor = ""; - document.body.style.userSelect = ""; - }; - - this.resizeHandle.addEventListener("mousedown", (e) => { - e.preventDefault(); - startY = e.clientY; - startHeight = this.dom.offsetHeight; - this.resizeHandle.classList.add("dragging"); - // 设置 body 样式,防止拖动时光标闪烁 - document.body.style.cursor = "ns-resize"; - document.body.style.userSelect = "none"; - document.addEventListener("mousemove", onMouseMove); - document.addEventListener("mouseup", onMouseUp); - }); - } - - /** - * 动态计算最大高度(编辑器高度) - */ - private getMaxHeight(): number { - return this.view.dom.clientHeight; - } - - /** - * 内部更新预览内容(带缓存 + DOM Diff 优化) - */ - private updateContentInternal(): void { - // 如果面板已销毁,直接返回 - if (this.isDestroyed) { - return; - } - - try { - const state = this.view.state; - const currentPreviewState = state.field(previewPanelState, false); - - if (!currentPreviewState) { - return; - } - - const blockContent = state.doc.sliceString( - currentPreviewState.blockFrom, - currentPreviewState.blockTo - ); - - if (!blockContent || blockContent.trim().length === 0) { - return; - } - - // 缓存检查:如果内容没变,不重新渲染 - if (blockContent === this.lastRenderedContent) { - return; - } - - // 对于大内容,使用异步渲染避免阻塞主线程 - if (blockContent.length > 1000) { - this.renderLargeContentAsync(blockContent); - } else { - // 小内容使用 DOM Diff 优化渲染 - this.renderWithDiff(blockContent); - } - - } catch (error) { - console.warn("Error updating preview content:", error); - } - } - - /** - * 使用 DOM Diff 渲染内容(保留未变化的节点) - */ - private renderWithDiff(content: string): void { - // 如果面板已销毁,直接返回 - if (this.isDestroyed) { - return; - } - - try { - const newHtml = this.md.render(content); - - // 如果是首次渲染或内容为空,直接设置 innerHTML - if (!this.lastRenderedContent || this.content.children.length === 0) { - this.content.innerHTML = newHtml; - } else { - // 使用 DOM Diff 增量更新 - morphHTML(this.content, newHtml); - } - - this.lastRenderedContent = content; - } catch (error) { - console.warn("Error rendering with diff:", error); - // 降级到直接设置 innerHTML - if (!this.isDestroyed) { - this.content.innerHTML = this.md.render(content); - this.lastRenderedContent = content; - } - } - } - - /** - * 异步渲染大内容(使用 DOM Diff 优化) - */ - private renderLargeContentAsync(content: string): void { - // 如果面板已销毁,直接返回 - if (this.isDestroyed) { - return; - } - - // 如果是首次渲染,显示加载状态 - if (!this.lastRenderedContent) { - this.content.innerHTML = '
Rendering...
'; - } - - // 使用 requestIdleCallback 在浏览器空闲时渲染 - const callback = window.requestIdleCallback || ((cb: IdleRequestCallback) => setTimeout(cb, 1)); - - callback(() => { - // 再次检查是否已销毁(异步回调时可能已经关闭) - if (this.isDestroyed) { - return; - } - - try { - const html = this.md.render(content); - - // 如果是首次渲染或之前内容为空,直接设置 - if (!this.lastRenderedContent || this.content.children.length === 0) { - // 使用 DocumentFragment 减少 DOM 操作 - const fragment = document.createRange().createContextualFragment(html); - this.content.innerHTML = ''; - this.content.appendChild(fragment); - } else { - // 使用 DOM Diff 增量更新(保留滚动位置和未变化的节点) - morphHTML(this.content, html); - } - - this.lastRenderedContent = content; - } catch (error) { - console.warn("Error rendering large content:", error); - if (!this.isDestroyed) { - this.content.innerHTML = '
Render failed
'; - } - } - }); - } - - private resetPreviewContent(): void { - if (this.isDestroyed) { - return; - } - - this.md = createMarkdownRenderer(); - this.lastRenderedContent = ""; - this.updateContentInternal(); - } - - /** - * 响应编辑器更新 - */ - public update(update: ViewUpdate): void { - if (update.docChanged) { - // 文档改变时使用防抖更新 - this.debouncedUpdate.debouncedFn(); - } else if (update.selectionSet) { - // 光标移动时不触发更新 - // 如果需要根据光标位置更新,可以在这里处理 - } - } - - /** - * 清理资源 - */ - public destroy(): void { - // 标记为已销毁,防止异步回调继续执行 - this.isDestroyed = true; - - // 清理防抖 - if (this.debouncedUpdate) { - this.debouncedUpdate.cancel(); - } - - // 清空缓存 - this.lastRenderedContent = ""; - - if (this.themeUnwatchers.length) { - this.themeUnwatchers.forEach(unwatch => unwatch()); - this.themeUnwatchers = []; - } - } - - /** - * 获取 CodeMirror Panel 对象 - */ - public getPanel(): Panel { - return { - top: false, - dom: this.dom, - update: (update: ViewUpdate) => this.update(update), - destroy: () => this.destroy() - }; - } -} - -/** - * 创建预览面板 - */ -export function createPreviewPanel(view: EditorView): Panel { - const panel = new MarkdownPreviewPanel(view); - return panel.getPanel(); -} - diff --git a/frontend/src/views/editor/extensions/markdownPreview/markdownRenderer.ts b/frontend/src/views/editor/extensions/markdownPreview/renderer.ts similarity index 100% rename from frontend/src/views/editor/extensions/markdownPreview/markdownRenderer.ts rename to frontend/src/views/editor/extensions/markdownPreview/renderer.ts diff --git a/frontend/src/views/editor/extensions/markdownPreview/state.ts b/frontend/src/views/editor/extensions/markdownPreview/state.ts deleted file mode 100644 index 306dd44..0000000 --- a/frontend/src/views/editor/extensions/markdownPreview/state.ts +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Markdown 预览面板的 CodeMirror 状态管理 - */ -import { EditorView, showPanel, ViewUpdate, ViewPlugin } from "@codemirror/view"; -import { StateEffect, StateField } from "@codemirror/state"; -import { getActiveNoteBlock } from "../codeblock/state"; -import { usePanelStore } from "@/stores/panelStore"; -import { createPreviewPanel } from "./panel"; -import type { PreviewState } from "./types"; - -/** - * 定义切换预览面板的 Effect - */ -export const togglePreview = StateEffect.define(); - -/** - * 关闭面板(带动画) - */ -export function closePreviewWithAnimation(view: EditorView): void { - const panelStore = usePanelStore(); - - // 标记开始关闭 - panelStore.startClosingMarkdownPreview(); - - const panelElement = view.dom.querySelector('.cm-panels.cm-panels-bottom') as HTMLElement; - if (panelElement) { - panelElement.style.animation = 'panelSlideDown 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; - // 等待动画完成后再关闭面板 - setTimeout(() => { - view.dispatch({ - effects: togglePreview.of(null) - }); - panelStore.closeMarkdownPreview(); - }, 280); - } else { - view.dispatch({ - effects: togglePreview.of(null) - }); - panelStore.closeMarkdownPreview(); - } -} - -/** - * 定义预览面板的状态字段 - */ -export const previewPanelState = StateField.define({ - create: () => null, - update(value, tr) { - const panelStore = usePanelStore(); - - for (let e of tr.effects) { - if (e.is(togglePreview)) { - value = e.value; - } - } - - // 如果有预览状态,智能管理预览生命周期 - if (value && !value.closing) { - const activeBlock = getActiveNoteBlock(tr.state as any); - - // 关键修复:检查预览状态是否属于当前文档 - // 如果 panelStore 中没有当前文档的预览状态(说明切换了文档), - // 则不执行关闭逻辑,保持其他文档的预览状态 - if (!panelStore.markdownPreview.isOpen) { - // 当前文档没有预览,不处理 - return value; - } - - // 场景1:离开 Markdown 块或无激活块 → 关闭预览 - if (!activeBlock || activeBlock.language.name.toLowerCase() !== 'md') { - if (!panelStore.markdownPreview.isClosing) { - return { ...value, closing: true }; - } - } - // 场景2:切换到其他块(起始位置变化)→ 关闭预览 - else if (activeBlock.content.from !== value.blockFrom) { - if (!panelStore.markdownPreview.isClosing) { - return { ...value, closing: true }; - } - } - // 场景3:还在同一个块内编辑(只有结束位置变化)→ 更新范围,实时预览 - else if (activeBlock.content.to !== value.blockTo) { - // 更新 panelStore 中的预览范围 - panelStore.updatePreviewRange(value.blockFrom, activeBlock.content.to); - - return { - documentId: value.documentId, - blockFrom: value.blockFrom, - blockTo: activeBlock.content.to, - closing: false - }; - } - } - - return value; - }, - provide: f => showPanel.from(f, state => state ? createPreviewPanel : null) -}); - -/** - * 创建监听插件 - */ -export const previewPanelPlugin = ViewPlugin.fromClass(class { - private lastState: PreviewState | null | undefined = null; - private panelStore = usePanelStore(); - - constructor(private view: EditorView) { - this.lastState = view.state.field(previewPanelState, false); - this.panelStore.setEditorView(view); - } - - update(update: ViewUpdate) { - const currentState = update.state.field(previewPanelState, false); - - // 检测到面板打开(从 null 变为有值,且不是 closing) - if (currentState && !currentState.closing && !this.lastState) { - // 验证面板 DOM 是否真正创建成功 - requestAnimationFrame(() => { - const panelElement = this.view.dom.querySelector('.cm-markdown-preview-panel'); - if (panelElement) { - // 面板创建成功,更新 store 状态 - this.panelStore.openMarkdownPreview(currentState.blockFrom, currentState.blockTo); - } - }); - } - - // 检测到状态变为 closing - if (currentState?.closing && !this.lastState?.closing) { - // 触发关闭动画 - closePreviewWithAnimation(this.view); - } - - this.lastState = currentState; - } - - destroy() { - // 不调用 reset(),因为那会清空所有文档的预览状态 - // 只清理编辑器视图引用 - this.panelStore.setEditorView(null); - } -}); - diff --git a/frontend/src/views/editor/extensions/markdownPreview/styles.ts b/frontend/src/views/editor/extensions/markdownPreview/styles.ts deleted file mode 100644 index 13a343c..0000000 --- a/frontend/src/views/editor/extensions/markdownPreview/styles.ts +++ /dev/null @@ -1,384 +0,0 @@ -import { EditorView } from "@codemirror/view"; -import type { ThemeColors } from "@/views/editor/theme/types"; - -/** - * 创建 Markdown 预览面板的主题样式 - */ -export function createMarkdownPreviewTheme(colors: ThemeColors) { - // GitHub 官方颜色变量 - const isDark = colors.dark; - - // GitHub Light 主题颜色 - const lightColors = { - fg: { - default: "#1F2328", - muted: "#656d76", - subtle: "#6e7781" - }, - border: { - default: "#d0d7de", - muted: "#d8dee4" - }, - canvas: { - default: "#ffffff", - subtle: "#f6f8fa" - }, - accent: { - fg: "#0969da", - emphasis: "#0969da" - } - }; - - // GitHub Dark 主题颜色 - const darkColors = { - fg: { - default: "#e6edf3", - muted: "#7d8590", - subtle: "#6e7681" - }, - border: { - default: "#30363d", - muted: "#21262d" - }, - canvas: { - default: "#0d1117", - subtle: "#161b22" - }, - accent: { - fg: "#2f81f7", - emphasis: "#2f81f7" - } - }; - - const ghColors = isDark ? darkColors : lightColors; - - return EditorView.theme({ - // 面板容器 - ".cm-markdown-preview-panel": { - position: "relative", - display: "flex", - flexDirection: "column", - overflow: "hidden" - }, - - // 拖动调整大小的手柄 - ".cm-preview-resize-handle": { - width: "100%", - height: "3px", - backgroundColor: colors.borderColor, - cursor: "ns-resize", - position: "relative", - flexShrink: 0, - transition: "background-color 0.2s ease", - "&:hover": { - backgroundColor: colors.selection - }, - "&.dragging": { - backgroundColor: colors.selection - } - }, - - // 面板动画效果 - '.cm-panels.cm-panels-top': { - borderBottom: '2px solid black' - }, - '.cm-panels.cm-panels-bottom': { - animation: 'panelSlideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1)' - }, - '@keyframes panelSlideUp': { - from: { - transform: 'translateY(100%)', - opacity: '0' - }, - to: { - transform: 'translateY(0)', - opacity: '1' - } - }, - '@keyframes panelSlideDown': { - from: { - transform: 'translateY(0)', - opacity: '1' - }, - to: { - transform: 'translateY(100%)', - opacity: '0' - } - }, - - // 内容区域 - ".cm-preview-content": { - flex: 1, - padding: "45px", - overflow: "auto", - fontSize: "16px", - lineHeight: "1.5", - color: ghColors.fg.default, - wordWrap: "break-word", - fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'", - boxSizing: "border-box", - - // Loading state - "& .markdown-loading, & .markdown-error": { - display: "flex", - alignItems: "center", - justifyContent: "center", - minHeight: "200px", - fontSize: "14px", - color: ghColors.fg.muted - }, - - "& .markdown-error": { - color: "#f85149" - }, - - // ========== 标题样式 ========== - "& h1, & h2, & h3, & h4, & h5, & h6": { - marginTop: "24px", - marginBottom: "16px", - fontWeight: "600", - lineHeight: "1.25", - color: ghColors.fg.default - }, - "& h1": { - fontSize: "2em", - borderBottom: `1px solid ${ghColors.border.muted}`, - paddingBottom: "0.3em" - }, - "& h2": { - fontSize: "1.5em", - borderBottom: `1px solid ${ghColors.border.muted}`, - paddingBottom: "0.3em" - }, - "& h3": { - fontSize: "1.25em" - }, - "& h4": { - fontSize: "1em" - }, - "& h5": { - fontSize: "0.875em" - }, - "& h6": { - fontSize: "0.85em", - color: ghColors.fg.muted - }, - - // ========== 段落和文本 ========== - "& p": { - marginTop: "0", - marginBottom: "16px" - }, - "& strong": { - fontWeight: "600" - }, - "& em": { - fontStyle: "italic" - }, - "& del": { - textDecoration: "line-through", - opacity: "0.7" - }, - - // ========== 列表 ========== - "& ul, & ol": { - paddingLeft: "2em", - marginTop: "0", - marginBottom: "16px" - }, - "& ul ul, & ul ol, & ol ol, & ol ul": { - marginTop: "0", - marginBottom: "0" - }, - "& li": { - wordWrap: "break-all" - }, - "& li > p": { - marginTop: "16px" - }, - "& li + li": { - marginTop: "0.25em" - }, - - // 任务列表 - "& .task-list-item": { - listStyleType: "none", - position: "relative", - paddingLeft: "1.5em" - }, - "& .task-list-item + .task-list-item": { - marginTop: "3px" - }, - "& .task-list-item input[type='checkbox']": { - font: "inherit", - overflow: "visible", - fontFamily: "inherit", - fontSize: "inherit", - lineHeight: "inherit", - boxSizing: "border-box", - padding: "0", - margin: "0 0.2em 0.25em -1.6em", - verticalAlign: "middle", - cursor: "pointer" - }, - - // ========== 代码块 ========== - "& code, & tt": { - fontFamily: "SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace", - fontSize: "85%", - padding: "0.2em 0.4em", - margin: "0", - backgroundColor: isDark ? "rgba(110, 118, 129, 0.4)" : "rgba(27, 31, 35, 0.05)", - borderRadius: "3px" - }, - - "& pre": { - position: "relative", - backgroundColor: isDark ? "#161b22" : "#f6f8fa", - padding: "40px 16px 16px 16px", - borderRadius: "6px", - overflow: "auto", - margin: "16px 0", - fontSize: "85%", - lineHeight: "1.45", - wordWrap: "normal", - - // macOS 窗口样式 - 使用伪元素创建顶部栏 - "&::before": { - content: '""', - position: "absolute", - top: "0", - left: "0", - right: "0", - height: "28px", - backgroundColor: isDark ? "#1c1c1e" : "#e8e8e8", - borderBottom: `1px solid ${ghColors.border.default}`, - borderRadius: "6px 6px 0 0" - }, - - // macOS 三个控制按钮 - "&::after": { - content: '""', - position: "absolute", - top: "10px", - left: "12px", - width: "12px", - height: "12px", - borderRadius: "50%", - backgroundColor: isDark ? "#ec6a5f" : "#ff5f57", - boxShadow: ` - 18px 0 0 0 ${isDark ? "#f4bf4f" : "#febc2e"}, - 36px 0 0 0 ${isDark ? "#61c554" : "#28c840"} - ` - } - }, - - "& pre code, & pre tt": { - display: "inline", - maxWidth: "auto", - padding: "0", - margin: "0", - overflow: "visible", - lineHeight: "inherit", - wordWrap: "normal", - backgroundColor: "transparent", - border: "0", - fontSize: "100%", - color: ghColors.fg.default, - wordBreak: "normal", - whiteSpace: "pre" - }, - - // ========== 引用块 ========== - "& blockquote": { - margin: "16px 0", - padding: "0 1em", - color: isDark ? "#7d8590" : "#6a737d", - borderLeft: isDark ? "0.25em solid #3b434b" : "0.25em solid #dfe2e5" - }, - "& blockquote > :first-child": { - marginTop: "0" - }, - "& blockquote > :last-child": { - marginBottom: "0" - }, - - // ========== 分割线 ========== - "& hr": { - height: "0.25em", - padding: "0", - margin: "24px 0", - backgroundColor: isDark ? "#21262d" : "#e1e4e8", - border: "0", - overflow: "hidden", - boxSizing: "content-box" - }, - - // ========== 表格 ========== - "& table": { - borderSpacing: "0", - borderCollapse: "collapse", - display: "block", - width: "100%", - overflow: "auto", - marginTop: "0", - marginBottom: "16px" - }, - "& table tr": { - backgroundColor: isDark ? "#0d1117" : "#ffffff", - borderTop: isDark ? "1px solid #21262d" : "1px solid #c6cbd1" - }, - "& table th, & table td": { - padding: "6px 13px", - border: isDark ? "1px solid #30363d" : "1px solid #dfe2e5" - }, - "& table th": { - fontWeight: "600" - }, - - // ========== 链接 ========== - "& a, & .markdown-link": { - color: isDark ? "#58a6ff" : "#0366d6", - textDecoration: "none", - cursor: "pointer", - "&:hover": { - textDecoration: "underline" - } - }, - - // ========== 图片 ========== - "& img": { - maxWidth: "100%", - height: "auto", - borderRadius: "4px", - margin: "16px 0" - }, - - // ========== 其他元素 ========== - "& kbd": { - display: "inline-block", - padding: "3px 5px", - fontSize: "11px", - lineHeight: "10px", - color: ghColors.fg.default, - verticalAlign: "middle", - backgroundColor: ghColors.canvas.subtle, - border: `solid 1px ${isDark ? "rgba(110, 118, 129, 0.4)" : "rgba(175, 184, 193, 0.2)"}`, - borderBottom: `solid 2px ${isDark ? "rgba(110, 118, 129, 0.4)" : "rgba(175, 184, 193, 0.2)"}`, - borderRadius: "6px", - boxShadow: "inset 0 -1px 0 rgba(175, 184, 193, 0.2)" - }, - - // 首个子元素去除上边距 - "& > *:first-child": { - marginTop: "0 !important" - }, - - // 最后一个子元素去除下边距 - "& > *:last-child": { - marginBottom: "0 !important" - } - } - }, { dark: colors.dark }); -} - diff --git a/frontend/src/views/editor/extensions/markdownPreview/types.ts b/frontend/src/views/editor/extensions/markdownPreview/types.ts deleted file mode 100644 index e30deec..0000000 --- a/frontend/src/views/editor/extensions/markdownPreview/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Markdown 预览面板相关类型定义 - */ - -// 预览面板状态 -export interface PreviewState { - documentId: number; // 预览所属的文档ID - blockFrom: number; - blockTo: number; - closing?: boolean; // 标记面板正在关闭 -} - diff --git a/frontend/src/views/editor/extensions/rainbowBracket/rainbowBracketsExtension.ts b/frontend/src/views/editor/extensions/rainbowBracket/index.ts similarity index 98% rename from frontend/src/views/editor/extensions/rainbowBracket/rainbowBracketsExtension.ts rename to frontend/src/views/editor/extensions/rainbowBracket/index.ts index 788e9a9..9926140 100644 --- a/frontend/src/views/editor/extensions/rainbowBracket/rainbowBracketsExtension.ts +++ b/frontend/src/views/editor/extensions/rainbowBracket/index.ts @@ -69,7 +69,7 @@ const rainbowBracketsPlugin = ViewPlugin.fromClass(RainbowBracketsView, { decorations: (v) => v.decorations, }); -export default function rainbowBracketsExtension() { +export default function index() { return [ rainbowBracketsPlugin, EditorView.baseTheme({ diff --git a/frontend/src/views/editor/manager/extensions.ts b/frontend/src/views/editor/manager/extensions.ts index 22d6317..9ea970a 100644 --- a/frontend/src/views/editor/manager/extensions.ts +++ b/frontend/src/views/editor/manager/extensions.ts @@ -3,7 +3,7 @@ import {ExtensionID} from '@/../bindings/voidraft/internal/models/models'; import i18n from '@/i18n'; import {ExtensionDefinition} from './types'; -import rainbowBracketsExtension from '../extensions/rainbowBracket/rainbowBracketsExtension'; +import index from '../extensions/rainbowBracket'; import {createTextHighlighter} from '../extensions/textHighlight'; import {color} from '../extensions/colorSelector'; import {hyperLink} from '../extensions/hyperlink'; @@ -28,7 +28,7 @@ const defineExtension = (create: (config: any) => any, defaultConfig: Record = { [ExtensionID.ExtensionRainbowBrackets]: { - definition: defineExtension(() => rainbowBracketsExtension()), + definition: defineExtension(() => index()), displayNameKey: 'extensions.rainbowBrackets.name', descriptionKey: 'extensions.rainbowBrackets.description' },