Fix/frontend review findings #4
@@ -1,14 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { Link } from '@element-plus/icons-vue'
|
||||
import { Crepe } from '@milkdown/crepe'
|
||||
import {
|
||||
createCodeBlockCommand,
|
||||
toggleEmphasisCommand,
|
||||
toggleInlineCodeCommand,
|
||||
toggleLinkCommand,
|
||||
toggleStrongCommand,
|
||||
wrapInBulletListCommand,
|
||||
wrapInHeadingCommand,
|
||||
wrapInOrderedListCommand,
|
||||
} 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 AppIcon from '@/components/common/AppIcon.vue'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { highlightCode } from '@/utils/markdown'
|
||||
@@ -23,7 +30,7 @@ const editorRoot = ref<HTMLElement | null>(null)
|
||||
const loading = ref(true)
|
||||
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) {
|
||||
const editor = crepe?.editor
|
||||
@@ -33,11 +40,35 @@ function runCommand(command: ToolbarCommand) {
|
||||
italic: callCommand(toggleEmphasisCommand.key),
|
||||
'ordered-list': callCommand(wrapInOrderedListCommand.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])
|
||||
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) {
|
||||
const level = Number((event.target as HTMLSelectElement).value)
|
||||
if (!level || !crepe) return
|
||||
@@ -64,6 +95,9 @@ onMounted(async () => {
|
||||
previewOnlyByDefault: true,
|
||||
previewLabel: 'Shiki 高亮预览',
|
||||
previewLoading: '正在高亮…',
|
||||
searchPlaceholder: '搜索语言',
|
||||
noResultText: '没有匹配的语言',
|
||||
copyText: '复制',
|
||||
previewToggleText: (previewOnlyMode) => previewOnlyMode ? '编辑代码' : '查看高亮',
|
||||
renderPreview: (language, content, applyPreview) => {
|
||||
void highlightCode(content, language).then((html) => {
|
||||
@@ -74,6 +108,42 @@ onMounted(async () => {
|
||||
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)
|
||||
@@ -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+I)" aria-label="斜体" @mousedown.prevent @click="runCommand('italic')"><em class="format-glyph">I</em></button>
|
||||
<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('bullet-list')">•</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')"><span class="list-marker">•</span><span class="list-lines">☰</span></button>
|
||||
<span class="toolbar-divider" />
|
||||
<label class="toolbar-select font-size-select" title="修改选中文字的字号">
|
||||
<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>
|
||||
</select>
|
||||
</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 v-if="loading" class="editor-loading">正在加载编辑器…</div>
|
||||
<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; }
|
||||
.heading-glyph { font-weight: 800; }
|
||||
.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 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; }
|
||||
@@ -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 .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); }
|
||||
: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(.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),
|
||||
|
||||
Reference in New Issue
Block a user