diff --git a/frontend/src/admin/AdminLayout.vue b/frontend/src/admin/AdminLayout.vue index ab56572..cbdff79 100644 --- a/frontend/src/admin/AdminLayout.vue +++ b/frontend/src/admin/AdminLayout.vue @@ -128,12 +128,12 @@ watch(() => route.path, () => loadCounts()) 查看前台 -
- -
diff --git a/frontend/src/admin/EditorView.vue b/frontend/src/admin/EditorView.vue index c3bce07..9113d47 100644 --- a/frontend/src/admin/EditorView.vue +++ b/frontend/src/admin/EditorView.vue @@ -369,21 +369,187 @@ function removeTag(t) { } // ---------- cover ---------- -const coverInput = ref('') - -function setCover() { - const v = coverInput.value.trim() - if (!v) return - form.cover_url = v - coverInput.value = '' -} - function clearCover() { form.cover_url = '' } // preview mode — render markdown to HTML for the side panel const previewHtml = computed(() => marked.parse(form.content_md || '', { breaks: true })) + +// ---------- 工具栏:Markdown 模式插入 + 通用动作 ---------- + +const mdPane = ref(null) +const showLinkInput = ref(false) +const showImageInput = ref(false) +const linkText = ref('') +const linkUrl = ref('') +const imageUrl = ref('') +const imageAlt = ref('') + +function insertAtCursor(text, selStart, selEnd) { + const before = form.content_md.slice(0, selStart) + const after = form.content_md.slice(selEnd) + form.content_md = before + text + after + nextTick(() => { + if (!mdPane.value) return + const newPos = selStart + text.length + mdPane.value.focus() + mdPane.value.setSelectionRange(newPos, newPos) + }) +} + +function withSelection(fn) { + const el = mdPane.value + const start = el ? el.selectionStart : form.content_md.length + const end = el ? el.selectionEnd : form.content_md.length + fn(start, end) +} + +function tbBold() { + withSelection((s, e) => { + const sel = form.content_md.slice(s, e) || '加粗' + insertAtCursor(`**${sel}**`, s, e) + }) +} +function tbItalic() { + withSelection((s, e) => { + const sel = form.content_md.slice(s, e) || '斜体' + insertAtCursor(`*${sel}*`, s, e) + }) +} +function tbStrike() { + withSelection((s, e) => { + const sel = form.content_md.slice(s, e) || '删除' + insertAtCursor(`~~${sel}~~`, s, e) + }) +} +function tbCode() { + withSelection((s, e) => { + const sel = form.content_md.slice(s, e) || 'code' + insertAtCursor('`' + sel + '`', s, e) + }) +} +function tbCodeBlock() { + withSelection((s, e) => { + const sel = form.content_md.slice(s, e) || '代码块' + insertAtCursor('\n```\n' + sel + '\n```\n', s, e) + }) +} +function tbH2() { + withSelection((s, e) => { + const sel = form.content_md.slice(s, e) || '二级标题' + insertAtCursor(`\n## ${sel}\n`, s, e) + }) +} +function tbH3() { + withSelection((s, e) => { + const sel = form.content_md.slice(s, e) || '三级标题' + insertAtCursor(`\n### ${sel}\n`, s, e) + }) +} +function tbQuote() { + withSelection((s, e) => { + const sel = form.content_md.slice(s, e) || '引用' + insertAtCursor('\n> ' + sel.replace(/\n/g, '\n> ') + '\n', s, e) + }) +} +function tbUl() { + withSelection((s, e) => { + const sel = form.content_md.slice(s, e) || '列表项' + insertAtCursor( + '\n' + sel.split('\n').map((l) => '- ' + l).join('\n') + '\n', + s, + e + ) + }) +} +function tbOl() { + withSelection((s, e) => { + const sel = form.content_md.slice(s, e) || '列表项' + insertAtCursor( + '\n' + + sel + .split('\n') + .map((l, i) => (i + 1) + '. ' + l) + .join('\n') + + '\n', + s, + e + ) + }) +} +function tbTask() { + withSelection((s, e) => { + const sel = form.content_md.slice(s, e) || '任务' + insertAtCursor('\n- [ ] ' + sel + '\n', s, e) + }) +} +function tbHr() { + withSelection((s, e) => insertAtCursor('\n---\n', s, e)) +} + +function openLink() { + showLinkInput.value = true + linkText.value = '' + linkUrl.value = '' +} +function commitLink() { + if (!linkUrl.value) return + const text = linkText.value || linkUrl.value + const md = `[${text}](${linkUrl.value})` + withSelection((s, e) => insertAtCursor(md, s, e)) + showLinkInput.value = false +} + +function openImage() { + showImageInput.value = true + imageUrl.value = '' + imageAlt.value = '' +} +function commitImage() { + if (!imageUrl.value) return + const md = `![${imageAlt.value || ''}](${imageUrl.value})` + withSelection((s, e) => insertAtCursor(md, s, e)) + showImageInput.value = false +} + +function insertDate() { + withSelection((s, e) => insertAtCursor(new Date().toISOString().slice(0, 10), s, e)) +} + +// 工具栏按钮的统一定义(用作 v-for 渲染) +const tbGroups = computed(() => [ + { + label: '格式', + items: [ + { key: 'b', label: 'B', title: '加粗', run: tbBold, show: mode.value === 'md' }, + { key: 'i', label: 'I', title: '斜体', run: tbItalic, italic: true, show: mode.value === 'md' }, + { key: 's', label: 'S', title: '删除线', run: tbStrike, show: mode.value === 'md' }, + { key: 'code', label: '<>', title: '行内代码', run: tbCode, mono: true, show: mode.value === 'md' }, + { key: 'cb', label: '```', title: '代码块', run: tbCodeBlock, mono: true, show: mode.value === 'md' } + ] + }, + { + label: '结构', + items: [ + { key: 'h2', label: 'H2', title: '二级标题', run: tbH2, show: mode.value === 'md' }, + { key: 'h3', label: 'H3', title: '三级标题', run: tbH3, show: mode.value === 'md' }, + { key: 'q', label: '"', title: '引用', run: tbQuote, show: mode.value === 'md' }, + { key: 'ul', label: '•', title: '无序列表', run: tbUl, show: mode.value === 'md' }, + { key: 'ol', label: '1.', title: '有序列表', run: tbOl, show: mode.value === 'md' }, + { key: 'task', label: '☐', title: '任务列表', run: tbTask, show: mode.value === 'md' }, + { key: 'hr', label: '—', title: '分隔线', run: tbHr, show: mode.value === 'md' } + ] + }, + { + label: '插入', + items: [ + { key: 'link', label: '🔗', title: '链接', run: openLink, show: mode.value === 'md' }, + { key: 'img', label: '🖼', title: '图片', run: openImage, show: mode.value === 'md' }, + { key: 'date', label: '📅', title: '插入今天日期', run: insertDate, show: mode.value === 'md' } + ] + } +])