feat(frontend): 扩展Markdown插入工具并汉化块菜单
This commit is contained in:
@@ -1,14 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
import { Link } from '@element-plus/icons-vue'
|
||||||
import { Crepe } from '@milkdown/crepe'
|
import { Crepe } from '@milkdown/crepe'
|
||||||
import {
|
import {
|
||||||
|
createCodeBlockCommand,
|
||||||
toggleEmphasisCommand,
|
toggleEmphasisCommand,
|
||||||
|
toggleInlineCodeCommand,
|
||||||
|
toggleLinkCommand,
|
||||||
toggleStrongCommand,
|
toggleStrongCommand,
|
||||||
wrapInBulletListCommand,
|
wrapInBulletListCommand,
|
||||||
wrapInHeadingCommand,
|
wrapInHeadingCommand,
|
||||||
wrapInOrderedListCommand,
|
wrapInOrderedListCommand,
|
||||||
} from '@milkdown/kit/preset/commonmark'
|
} from '@milkdown/kit/preset/commonmark'
|
||||||
|
import { commandsCtx, editorViewCtx } from '@milkdown/kit/core'
|
||||||
|
import { TextSelection } from '@milkdown/kit/prose/state'
|
||||||
import { callCommand } from '@milkdown/kit/utils'
|
import { callCommand } from '@milkdown/kit/utils'
|
||||||
|
import AppIcon from '@/components/common/AppIcon.vue'
|
||||||
import { useEditorStore } from '@/stores/editor'
|
import { useEditorStore } from '@/stores/editor'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import { highlightCode } from '@/utils/markdown'
|
import { highlightCode } from '@/utils/markdown'
|
||||||
@@ -23,7 +30,7 @@ const editorRoot = ref<HTMLElement | null>(null)
|
|||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
let crepe: Crepe | null = null
|
let crepe: Crepe | null = null
|
||||||
|
|
||||||
type ToolbarCommand = 'bold' | 'italic' | 'ordered-list' | 'bullet-list'
|
type ToolbarCommand = 'bold' | 'italic' | 'ordered-list' | 'bullet-list' | 'inline-code' | 'code-block' | 'inline-math' | 'math-block'
|
||||||
|
|
||||||
function runCommand(command: ToolbarCommand) {
|
function runCommand(command: ToolbarCommand) {
|
||||||
const editor = crepe?.editor
|
const editor = crepe?.editor
|
||||||
@@ -33,11 +40,35 @@ function runCommand(command: ToolbarCommand) {
|
|||||||
italic: callCommand(toggleEmphasisCommand.key),
|
italic: callCommand(toggleEmphasisCommand.key),
|
||||||
'ordered-list': callCommand(wrapInOrderedListCommand.key),
|
'ordered-list': callCommand(wrapInOrderedListCommand.key),
|
||||||
'bullet-list': callCommand(wrapInBulletListCommand.key),
|
'bullet-list': callCommand(wrapInBulletListCommand.key),
|
||||||
|
'inline-code': callCommand(toggleInlineCodeCommand.key),
|
||||||
|
'code-block': callCommand(createCodeBlockCommand.key, ''),
|
||||||
|
'inline-math': callCommand('ToggleLatex'),
|
||||||
|
'math-block': callCommand(createCodeBlockCommand.key, 'LaTeX'),
|
||||||
}
|
}
|
||||||
editor.action(actions[command])
|
editor.action(actions[command])
|
||||||
editorRoot.value?.querySelector<HTMLElement>('.ProseMirror')?.focus()
|
editorRoot.value?.querySelector<HTMLElement>('.ProseMirror')?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyLink() {
|
||||||
|
if (!crepe) return
|
||||||
|
const href = window.prompt('请输入链接地址', 'https://')?.trim()
|
||||||
|
if (!href) return
|
||||||
|
|
||||||
|
crepe.editor.action((ctx) => {
|
||||||
|
const view = ctx.get(editorViewCtx)
|
||||||
|
const commands = ctx.get(commandsCtx)
|
||||||
|
if (view.state.selection.empty) {
|
||||||
|
const label = window.prompt('请输入链接文字', href)?.trim() || href
|
||||||
|
const from = view.state.selection.from
|
||||||
|
const transaction = view.state.tr.insertText(label, from)
|
||||||
|
transaction.setSelection(TextSelection.create(transaction.doc, from, from + label.length))
|
||||||
|
view.dispatch(transaction)
|
||||||
|
}
|
||||||
|
return commands.call(toggleLinkCommand.key, { href })
|
||||||
|
})
|
||||||
|
editorRoot.value?.querySelector<HTMLElement>('.ProseMirror')?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
function applyHeading(event: Event) {
|
function applyHeading(event: Event) {
|
||||||
const level = Number((event.target as HTMLSelectElement).value)
|
const level = Number((event.target as HTMLSelectElement).value)
|
||||||
if (!level || !crepe) return
|
if (!level || !crepe) return
|
||||||
@@ -64,6 +95,9 @@ onMounted(async () => {
|
|||||||
previewOnlyByDefault: true,
|
previewOnlyByDefault: true,
|
||||||
previewLabel: 'Shiki 高亮预览',
|
previewLabel: 'Shiki 高亮预览',
|
||||||
previewLoading: '正在高亮…',
|
previewLoading: '正在高亮…',
|
||||||
|
searchPlaceholder: '搜索语言',
|
||||||
|
noResultText: '没有匹配的语言',
|
||||||
|
copyText: '复制',
|
||||||
previewToggleText: (previewOnlyMode) => previewOnlyMode ? '编辑代码' : '查看高亮',
|
previewToggleText: (previewOnlyMode) => previewOnlyMode ? '编辑代码' : '查看高亮',
|
||||||
renderPreview: (language, content, applyPreview) => {
|
renderPreview: (language, content, applyPreview) => {
|
||||||
void highlightCode(content, language).then((html) => {
|
void highlightCode(content, language).then((html) => {
|
||||||
@@ -74,6 +108,42 @@ onMounted(async () => {
|
|||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[Crepe.Feature.Latex]: {
|
||||||
|
inlineEditConfirm: '确认',
|
||||||
|
},
|
||||||
|
[Crepe.Feature.LinkTooltip]: {
|
||||||
|
editButton: '编辑',
|
||||||
|
removeButton: '移除',
|
||||||
|
confirmButton: '确认',
|
||||||
|
inputPlaceholder: '粘贴链接地址…',
|
||||||
|
},
|
||||||
|
[Crepe.Feature.BlockEdit]: {
|
||||||
|
textGroup: {
|
||||||
|
label: '文本',
|
||||||
|
text: { label: '正文' },
|
||||||
|
h1: { label: '一级标题' },
|
||||||
|
h2: { label: '二级标题' },
|
||||||
|
h3: { label: '三级标题' },
|
||||||
|
h4: { label: '四级标题' },
|
||||||
|
h5: { label: '五级标题' },
|
||||||
|
h6: { label: '六级标题' },
|
||||||
|
quote: { label: '引用' },
|
||||||
|
divider: { label: '分割线' },
|
||||||
|
},
|
||||||
|
listGroup: {
|
||||||
|
label: '列表',
|
||||||
|
bulletList: { label: '无序列表' },
|
||||||
|
orderedList: { label: '有序列表' },
|
||||||
|
taskList: { label: '任务列表' },
|
||||||
|
},
|
||||||
|
advancedGroup: {
|
||||||
|
label: '插入',
|
||||||
|
image: { label: '图片' },
|
||||||
|
codeBlock: { label: '代码块' },
|
||||||
|
table: { label: '表格' },
|
||||||
|
math: { label: '公式块' },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
crepe.editor.use(fontSizeMarkdownPlugin)
|
crepe.editor.use(fontSizeMarkdownPlugin)
|
||||||
@@ -104,8 +174,8 @@ onBeforeUnmount(() => { void crepe?.destroy() })
|
|||||||
<button type="button" title="加粗 (Ctrl+B)" aria-label="加粗" @mousedown.prevent @click="runCommand('bold')"><strong class="format-glyph">B</strong></button>
|
<button type="button" title="加粗 (Ctrl+B)" aria-label="加粗" @mousedown.prevent @click="runCommand('bold')"><strong class="format-glyph">B</strong></button>
|
||||||
<button type="button" title="斜体 (Ctrl+I)" aria-label="斜体" @mousedown.prevent @click="runCommand('italic')"><em class="format-glyph">I</em></button>
|
<button type="button" title="斜体 (Ctrl+I)" aria-label="斜体" @mousedown.prevent @click="runCommand('italic')"><em class="format-glyph">I</em></button>
|
||||||
<span class="toolbar-divider" />
|
<span class="toolbar-divider" />
|
||||||
<button type="button" class="list-glyph" title="有序列表" aria-label="有序列表" @mousedown.prevent @click="runCommand('ordered-list')">1.</button>
|
<button type="button" class="list-glyph" title="有序列表" aria-label="有序列表" @mousedown.prevent @click="runCommand('ordered-list')"><span class="list-marker">1</span><span class="list-lines">☰</span></button>
|
||||||
<button type="button" class="list-glyph" title="无序列表" aria-label="无序列表" @mousedown.prevent @click="runCommand('bullet-list')">•</button>
|
<button type="button" class="list-glyph" title="无序列表" aria-label="无序列表" @mousedown.prevent @click="runCommand('bullet-list')"><span class="list-marker">•</span><span class="list-lines">☰</span></button>
|
||||||
<span class="toolbar-divider" />
|
<span class="toolbar-divider" />
|
||||||
<label class="toolbar-select font-size-select" title="修改选中文字的字号">
|
<label class="toolbar-select font-size-select" title="修改选中文字的字号">
|
||||||
<span class="format-glyph font-size-glyph">A</span>
|
<span class="format-glyph font-size-glyph">A</span>
|
||||||
@@ -114,6 +184,12 @@ onBeforeUnmount(() => { void crepe?.destroy() })
|
|||||||
<option v-for="size in [12, 14, 16, 18, 20, 24, 28, 32]" :key="size" :value="size">{{ size }} px</option>
|
<option v-for="size in [12, 14, 16, 18, 20, 24, 28, 32]" :key="size" :value="size">{{ size }} px</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<span class="toolbar-divider" />
|
||||||
|
<button type="button" title="行内代码" aria-label="行内代码" @mousedown.prevent @click="runCommand('inline-code')"><code class="code-glyph"></></code></button>
|
||||||
|
<button type="button" title="代码块" aria-label="代码块" @mousedown.prevent @click="runCommand('code-block')"><span class="block-glyph">{ }</span></button>
|
||||||
|
<button type="button" title="行内公式" aria-label="行内公式" @mousedown.prevent @click="runCommand('inline-math')"><span class="math-glyph">ƒx</span></button>
|
||||||
|
<button type="button" title="公式块" aria-label="公式块" @mousedown.prevent @click="runCommand('math-block')"><span class="math-glyph">∑</span></button>
|
||||||
|
<button type="button" title="插入链接" aria-label="插入链接" @mousedown.prevent @click="applyLink"><AppIcon :icon="Link" :size="17" /></button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="loading" class="editor-loading">正在加载编辑器…</div>
|
<div v-if="loading" class="editor-loading">正在加载编辑器…</div>
|
||||||
<div ref="editorRoot" class="milkdown-host" :class="{ loading }" />
|
<div ref="editorRoot" class="milkdown-host" :class="{ loading }" />
|
||||||
@@ -129,7 +205,11 @@ onBeforeUnmount(() => { void crepe?.destroy() })
|
|||||||
.format-glyph { font-family: Georgia, 'Times New Roman', serif; font-size: 17px; line-height: 1; }
|
.format-glyph { font-family: Georgia, 'Times New Roman', serif; font-size: 17px; line-height: 1; }
|
||||||
.heading-glyph { font-weight: 800; }
|
.heading-glyph { font-weight: 800; }
|
||||||
.font-size-glyph { font-size: 18px; }
|
.font-size-glyph { font-size: 18px; }
|
||||||
.list-glyph { font-size: 17px; font-weight: 700; }
|
.list-glyph { grid-template-columns: 8px 14px; column-gap: 2px; font-weight: 700; }
|
||||||
|
.list-marker { font: 700 12px/1 var(--font-ui-sans); }
|
||||||
|
.list-lines { overflow: hidden; width: 14px; font-size: 15px; line-height: 1; transform: scaleX(1.2); }
|
||||||
|
.code-glyph, .block-glyph { padding: 0; background: transparent; color: inherit; font: 700 13px/1 var(--font-editor-mono); }
|
||||||
|
.math-glyph { font: italic 700 16px/1 Georgia, 'Times New Roman', serif; }
|
||||||
.toolbar-select { display: inline-flex; align-items: center; gap: 4px; min-height: 30px; padding: 3px 5px 3px 8px; border-radius: var(--radius-sm); color: var(--color-text-primary); }
|
.toolbar-select { display: inline-flex; align-items: center; gap: 4px; min-height: 30px; padding: 3px 5px 3px 8px; border-radius: var(--radius-sm); color: var(--color-text-primary); }
|
||||||
.toolbar-select select { min-width: 58px; border: 0; outline: 0; background: transparent; color: inherit; cursor: pointer; font-size: var(--font-size-sm); }
|
.toolbar-select select { min-width: 58px; border: 0; outline: 0; background: transparent; color: inherit; cursor: pointer; font-size: var(--font-size-sm); }
|
||||||
.font-size-select select { min-width: 62px; }
|
.font-size-select select { min-width: 62px; }
|
||||||
@@ -168,6 +248,10 @@ onBeforeUnmount(() => { void crepe?.destroy() })
|
|||||||
.milkdown-host :deep(.milkdown-toolbar) { border: 1px solid var(--color-border-default); background: var(--color-surface-elevated); box-shadow: var(--shadow-md); }
|
.milkdown-host :deep(.milkdown-toolbar) { border: 1px solid var(--color-border-default); background: var(--color-surface-elevated); box-shadow: var(--shadow-md); }
|
||||||
.milkdown-host :deep(.milkdown-toolbar .toolbar-item svg), .milkdown-host :deep(.milkdown-toolbar .toolbar-item.active svg) { color: var(--color-text-primary); fill: var(--color-text-primary); stroke: currentColor; opacity: 1; }
|
.milkdown-host :deep(.milkdown-toolbar .toolbar-item svg), .milkdown-host :deep(.milkdown-toolbar .toolbar-item.active svg) { color: var(--color-text-primary); fill: var(--color-text-primary); stroke: currentColor; opacity: 1; }
|
||||||
.milkdown-host :deep(.milkdown-toolbar .toolbar-item:hover svg) { color: var(--color-accent-primary); fill: var(--color-accent-primary); }
|
.milkdown-host :deep(.milkdown-toolbar .toolbar-item:hover svg) { color: var(--color-accent-primary); fill: var(--color-accent-primary); }
|
||||||
|
:global([data-theme='light']) .milkdown-host :deep(.milkdown-table-block th),
|
||||||
|
:global([data-theme='light']) .milkdown-host :deep(.milkdown-table-block td) { border-color: var(--color-text-tertiary); }
|
||||||
|
:global([data-theme='light']) .milkdown-host :deep(.milkdown-list-item-block li .label-wrapper) { color: var(--color-text-secondary); font-weight: 600; }
|
||||||
|
:global([data-theme='light']) .milkdown-host :deep(.milkdown-list-item-block li .label-wrapper svg) { fill: var(--color-text-secondary); }
|
||||||
.milkdown-host :deep(code) { font-family: var(--font-editor-mono); }
|
.milkdown-host :deep(code) { font-family: var(--font-editor-mono); }
|
||||||
.milkdown-host :deep(.shiki) { box-sizing: border-box; width: 100%; overflow: auto; padding: var(--space-md); border-radius: var(--radius-md); }
|
.milkdown-host :deep(.shiki) { box-sizing: border-box; width: 100%; overflow: auto; padding: var(--space-md); border-radius: var(--radius-md); }
|
||||||
:global([data-theme='dark']) .milkdown-host :deep(.shiki),
|
:global([data-theme='dark']) .milkdown-host :deep(.shiki),
|
||||||
|
|||||||
Reference in New Issue
Block a user