Fix/frontend review findings #4

Merged
Kronecker merged 23 commits from fix/frontend-review-findings into main 2026-08-30 15:06:05 +08:00
Showing only changes of commit d7a640a147 - Show all commits
@@ -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">&lt;/&gt;</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),