From bd12267d218d7e6a6e7eced5eb4d6a481d10b2da Mon Sep 17 00:00:00 2001 From: Teror Fox Date: Tue, 17 Feb 2026 11:30:57 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Shiroi=20?= =?UTF-8?q?=E6=89=A9=E5=B1=95=E8=AF=AD=E6=B3=95=E5=92=8C=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E5=9D=97=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../editor/slash-menu/slash-menu-items.ts | 416 ++++++++++++++++++ 1 file changed, 416 insertions(+) diff --git a/src/components/editor/slash-menu/slash-menu-items.ts b/src/components/editor/slash-menu/slash-menu-items.ts index f0e45cf42..21f6868ca 100644 --- a/src/components/editor/slash-menu/slash-menu-items.ts +++ b/src/components/editor/slash-menu/slash-menu-items.ts @@ -1,25 +1,42 @@ import { + AlertCircle, + AlertTriangle, Bold, ChevronDown, Code, Code2, + Flag, + Grid3x3, + Hash, Heading1, Heading2, Heading3, Heading4, Heading5, Heading6, + Highlighter, Image, + Images, + Info, Italic, + Lightbulb, Link, + Link2, List, ListChecks, ListOrdered, + MessageSquare, Minus, + Paintbrush, + Plus, Quote, + ShieldAlert, Sigma, + Sparkles, Strikethrough, Table, + User, + Zap, } from 'lucide-vue-next' import type { EditorView } from '@codemirror/view' import type { Component } from 'vue' @@ -114,6 +131,235 @@ const insertMathBlock = (view: EditorView): boolean => { return true } +// Shiroi 扩展语法 +const insertAlert = + (type: 'NOTE' | 'TIP' | 'IMPORTANT' | 'WARNING' | 'CAUTION') => + (view: EditorView): boolean => { + const { state } = view + const { from } = state.selection.main + const line = state.doc.lineAt(from) + const insertPos = line.to + const needsNewline = line.text.length > 0 + const insert = `${needsNewline ? '\n' : ''}> [!${type}]\n> 在此输入内容\n\n` + const cursorOffset = insert.indexOf('在此输入内容') + + view.dispatch({ + changes: { from: insertPos, to: insertPos, insert }, + selection: { anchor: insertPos + cursorOffset }, + }) + + view.focus() + return true + } + +const insertSpoiler = (view: EditorView): boolean => { + const { state } = view + const { from, to } = state.selection.main + const selectedText = state.sliceDoc(from, to) + const content = selectedText || '剧透内容' + const insert = `||${content}||` + + view.dispatch({ + changes: { from, to, insert }, + selection: + selectedText.length > 0 + ? { anchor: from + insert.length } + : { anchor: from + 2, head: from + 2 + content.length }, + }) + + view.focus() + return true +} + +const insertMention = + (platform: 'GH' | 'TW' | 'TG') => + (view: EditorView): boolean => { + const { state } = view + const { from, to } = state.selection.main + const selectedText = state.sliceDoc(from, to) + const handle = selectedText || 'username' + const insert = `{${platform}@${handle}}` + + view.dispatch({ + changes: { from, to, insert }, + selection: + selectedText.length > 0 + ? { anchor: from + insert.length } + : { + anchor: from + platform.length + 2, + head: from + platform.length + 2 + handle.length, + }, + }) + + view.focus() + return true + } + +const insertContainer = + (type: string, placeholder = '在此输入内容') => + (view: EditorView): boolean => { + const { state } = view + const { from } = state.selection.main + const line = state.doc.lineAt(from) + const insertPos = line.to + const needsNewline = line.text.length > 0 + const insert = `${needsNewline ? '\n' : ''}::: ${type}\n${placeholder}\n:::\n\n` + const cursorOffset = insert.indexOf(placeholder) + + view.dispatch({ + changes: { from: insertPos, to: insertPos, insert }, + selection: { anchor: insertPos + cursorOffset }, + }) + + view.focus() + return true + } + +const insertGallery = (view: EditorView): boolean => { + const { state } = view + const { from } = state.selection.main + const line = state.doc.lineAt(from) + const insertPos = line.to + const needsNewline = line.text.length > 0 + const insert = `${needsNewline ? '\n' : ''}::: gallery\nhttps://example.com/image1.jpg\nhttps://example.com/image2.jpg\n:::\n\n` + const cursorOffset = insert.indexOf('https://example.com/image1.jpg') + + view.dispatch({ + changes: { from: insertPos, to: insertPos, insert }, + selection: { anchor: insertPos + cursorOffset }, + }) + + view.focus() + return true +} + +const insertGrid = (view: EditorView): boolean => { + const { state } = view + const { from } = state.selection.main + const line = state.doc.lineAt(from) + const insertPos = line.to + const needsNewline = line.text.length > 0 + const insert = `${needsNewline ? '\n' : ''}::: grid {cols=3,gap=4}\n内容1\n\n内容2\n\n内容3\n:::\n\n` + const cursorOffset = insert.indexOf('内容1') + + view.dispatch({ + changes: { from: insertPos, to: insertPos, insert }, + selection: { anchor: insertPos + cursorOffset }, + }) + + view.focus() + return true +} + +const insertLinkCard = (view: EditorView): boolean => { + const { state } = view + const { from } = state.selection.main + const line = state.doc.lineAt(from) + const insertPos = line.to + const needsNewline = line.text.length > 0 + const insert = `${needsNewline ? '\n' : ''}\n\n` + const cursorOffset = insert.indexOf('username/repo') + + view.dispatch({ + changes: { from: insertPos, to: insertPos, insert }, + selection: { + anchor: insertPos + cursorOffset, + head: insertPos + cursorOffset + 'username/repo'.length, + }, + }) + + view.focus() + return true +} + +const insertExcalidraw = (view: EditorView): boolean => { + const { state } = view + const { from } = state.selection.main + const line = state.doc.lineAt(from) + const insertPos = line.to + const needsNewline = line.text.length > 0 + const insert = `${needsNewline ? '\n' : ''}\`\`\`excalidraw\n{}\n\`\`\`\n\n` + const cursorOffset = insert.indexOf('{}') + 1 + + view.dispatch({ + changes: { from: insertPos, to: insertPos, insert }, + selection: { anchor: insertPos + cursorOffset }, + }) + + view.focus() + return true +} + +const insertReactComponent = (view: EditorView): boolean => { + const { state } = view + const { from } = state.selection.main + const line = state.doc.lineAt(from) + const insertPos = line.to + const needsNewline = line.text.length > 0 + const insert = `${needsNewline ? '\n' : ''}\`\`\`component\nimport=https://cdn.example.com/component.js\nname=Component.Name\n\`\`\`\n\n` + const cursorOffset = insert.indexOf('https://cdn.example.com/component.js') + + view.dispatch({ + changes: { from: insertPos, to: insertPos, insert }, + selection: { anchor: insertPos + cursorOffset }, + }) + + view.focus() + return true +} + +const insertMark = (view: EditorView): boolean => { + const { state } = view + const { from, to } = state.selection.main + const selectedText = state.sliceDoc(from, to) + const content = selectedText || '高亮文本' + const insert = `==${content}==` + + view.dispatch({ + changes: { from, to, insert }, + selection: + selectedText.length > 0 + ? { anchor: from + insert.length } + : { anchor: from + 2, head: from + 2 + content.length }, + }) + + view.focus() + return true +} + +const insertInsertMark = (view: EditorView): boolean => { + const { state } = view + const { from, to } = state.selection.main + const selectedText = state.sliceDoc(from, to) + const content = selectedText || '插入文本' + const insert = `++${content}++` + + view.dispatch({ + changes: { from, to, insert }, + selection: + selectedText.length > 0 + ? { anchor: from + insert.length } + : { anchor: from + 2, head: from + 2 + content.length }, + }) + + view.focus() + return true +} + +const insertFootnote = (view: EditorView): boolean => { + const { state } = view + const { from, to } = state.selection.main + const insert = `[^1]` + + view.dispatch({ + changes: { from, to, insert }, + selection: { anchor: from + 2, head: from + 3 }, + }) + + view.focus() + return true +} + export const slashMenuGroups: SlashMenuGroup[] = [ { id: 'heading', @@ -313,6 +559,176 @@ export const slashMenuGroups: SlashMenuGroup[] = [ }, ], }, + { + id: 'shiroi-alerts', + label: 'Shiroi 提示块', + items: [ + { + id: 'alert-note', + label: 'Note 提示', + description: '>[!NOTE] 提示信息', + icon: Info, + keywords: ['note', 'info', 'alert', '提示'], + command: insertAlert('NOTE'), + }, + { + id: 'alert-tip', + label: 'Tip 建议', + description: '>[!TIP] 有用的建议', + icon: Lightbulb, + keywords: ['tip', 'hint', 'alert', '建议', '提示'], + command: insertAlert('TIP'), + }, + { + id: 'alert-important', + label: 'Important 重要', + description: '>[!IMPORTANT] 重要信息', + icon: AlertCircle, + keywords: ['important', 'alert', '重要'], + command: insertAlert('IMPORTANT'), + }, + { + id: 'alert-warning', + label: 'Warning 警告', + description: '>[!WARNING] 警告信息', + icon: AlertTriangle, + keywords: ['warning', 'alert', '警告'], + command: insertAlert('WARNING'), + }, + { + id: 'alert-caution', + label: 'Caution 注意', + description: '>[!CAUTION] 需要注意', + icon: ShieldAlert, + keywords: ['caution', 'danger', 'alert', '注意', '危险'], + command: insertAlert('CAUTION'), + }, + ], + }, + { + id: 'shiroi-inline', + label: 'Shiroi 行内语法', + items: [ + { + id: 'spoiler', + label: '剧透文本', + description: '||隐藏的内容||', + icon: MessageSquare, + keywords: ['spoiler', 'hidden', '剧透', '隐藏'], + command: insertSpoiler, + }, + { + id: 'mention-github', + label: 'GitHub 提及', + description: '{GH@username}', + icon: User, + keywords: ['mention', 'github', 'gh', '@', '提及'], + command: insertMention('GH'), + }, + { + id: 'mention-twitter', + label: 'Twitter 提及', + description: '{TW@username}', + icon: User, + keywords: ['mention', 'twitter', 'tw', '@', '提及'], + command: insertMention('TW'), + }, + { + id: 'mention-telegram', + label: 'Telegram 提及', + description: '{TG@username}', + icon: User, + keywords: ['mention', 'telegram', 'tg', '@', '提及'], + command: insertMention('TG'), + }, + { + id: 'mark', + label: '高亮标记', + description: '==高亮文本==', + icon: Highlighter, + keywords: ['mark', 'highlight', '高亮', '标记'], + command: insertMark, + }, + { + id: 'insert', + label: '插入标记', + description: '++插入文本++', + icon: Plus, + keywords: ['insert', 'add', '插入', '添加'], + command: insertInsertMark, + }, + { + id: 'footnote', + label: '脚注', + description: '[^1]', + icon: Hash, + keywords: ['footnote', 'reference', '脚注', '引用'], + command: insertFootnote, + }, + ], + }, + { + id: 'shiroi-advanced', + label: 'Shiroi 高级块', + items: [ + { + id: 'container-warning', + label: 'Container 警告', + description: '::: warning', + icon: Flag, + keywords: ['container', 'warning', '容器', '警告'], + command: insertContainer('warning'), + }, + { + id: 'container-banner', + label: 'Banner 横幅', + description: '::: banner {error}', + icon: Zap, + keywords: ['banner', 'container', '横幅', '容器'], + command: insertContainer('banner {error}'), + }, + { + id: 'gallery', + label: 'Gallery 画廊', + description: '::: gallery 图片集', + icon: Images, + keywords: ['gallery', 'images', '画廊', '图片集'], + command: insertGallery, + }, + { + id: 'grid', + label: 'Grid 网格', + description: '::: grid 网格布局', + icon: Grid3x3, + keywords: ['grid', 'layout', '网格', '布局'], + command: insertGrid, + }, + { + id: 'linkcard', + label: 'LinkCard 链接卡片', + description: ' 卡片', + icon: Link2, + keywords: ['linkcard', 'card', 'link', '链接卡片', '卡片'], + command: insertLinkCard, + }, + { + id: 'excalidraw', + label: 'Excalidraw 手绘', + description: '```excalidraw 手绘图', + icon: Paintbrush, + keywords: ['excalidraw', 'draw', 'whiteboard', '手绘', '画板'], + command: insertExcalidraw, + }, + { + id: 'react-component', + label: 'React 组件', + description: '```component 远程组件', + icon: Sparkles, + keywords: ['component', 'react', 'remote', '组件', '远程'], + command: insertReactComponent, + }, + ], + }, ] export interface SlashMenuItemWithGroup extends SlashMenuItem { From 1c0efed996db9549d9e7678d9bb8593d6a94343c Mon Sep 17 00:00:00 2001 From: Teror Fox Date: Tue, 17 Feb 2026 12:12:48 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E6=A1=86=E5=8A=9F=E8=83=BD=E4=BB=A5=E6=8F=92=E5=85=A5?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E3=80=81=E5=89=A7=E9=80=8F=E3=80=81=E6=8F=90?= =?UTF-8?q?=E5=8F=8A=E3=80=81=E7=BD=91=E6=A0=BC=E5=B8=83=E5=B1=80=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../editor/slash-menu/slash-menu-items.ts | 387 ++++++++++++++---- 1 file changed, 314 insertions(+), 73 deletions(-) diff --git a/src/components/editor/slash-menu/slash-menu-items.ts b/src/components/editor/slash-menu/slash-menu-items.ts index 21f6868ca..3fbb40c07 100644 --- a/src/components/editor/slash-menu/slash-menu-items.ts +++ b/src/components/editor/slash-menu/slash-menu-items.ts @@ -38,6 +38,8 @@ import { User, Zap, } from 'lucide-vue-next' +import { NButton, NInput, NSelect } from 'naive-ui' +import { h, ref } from 'vue' import type { EditorView } from '@codemirror/view' import type { Component } from 'vue' @@ -58,19 +60,160 @@ export interface SlashMenuGroup { items: SlashMenuItem[] } +// 对话框辅助函数 +const showInputDialog = ( + title: string, + placeholder: string, + defaultValue = '', + onConfirm: (value: string) => void, +) => { + const inputValue = ref(defaultValue) + const $dialog = window.dialog.create({ + title, + content: () => + h(NInput, { + value: inputValue.value, + placeholder, + autofocus: true, + onUpdateValue: (v: string) => { + inputValue.value = v + }, + onKeydown: (e: KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + $dialog.destroy() + onConfirm(inputValue.value) + } + }, + }), + action: () => + h('div', { class: 'flex gap-2 justify-end' }, [ + h( + NButton, + { + onClick: () => $dialog.destroy(), + }, + { default: () => '取消' }, + ), + h( + NButton, + { + type: 'primary', + onClick: () => { + $dialog.destroy() + onConfirm(inputValue.value) + }, + }, + { default: () => '确定' }, + ), + ]), + }) +} + +const showTextareaDialog = ( + title: string, + placeholder: string, + defaultValue = '', + onConfirm: (value: string) => void, +) => { + const inputValue = ref(defaultValue) + const $dialog = window.dialog.create({ + title, + content: () => + h(NInput, { + value: inputValue.value, + placeholder, + type: 'textarea', + rows: 4, + autofocus: true, + onUpdateValue: (v: string) => { + inputValue.value = v + }, + }), + action: () => + h('div', { class: 'flex gap-2 justify-end' }, [ + h( + NButton, + { + onClick: () => $dialog.destroy(), + }, + { default: () => '取消' }, + ), + h( + NButton, + { + type: 'primary', + onClick: () => { + $dialog.destroy() + onConfirm(inputValue.value) + }, + }, + { default: () => '确定' }, + ), + ]), + }) +} + +const showSelectDialog = ( + title: string, + options: { label: string; value: string }[], + defaultValue: string, + onConfirm: (value: string) => void, +) => { + const selectedValue = ref(defaultValue) + const $dialog = window.dialog.create({ + title, + content: () => + h(NSelect, { + value: selectedValue.value, + options, + onUpdateValue: (v: string) => { + selectedValue.value = v + }, + }), + action: () => + h('div', { class: 'flex gap-2 justify-end' }, [ + h( + NButton, + { + onClick: () => $dialog.destroy(), + }, + { default: () => '取消' }, + ), + h( + NButton, + { + type: 'primary', + onClick: () => { + $dialog.destroy() + onConfirm(selectedValue.value) + }, + }, + { default: () => '确定' }, + ), + ]), + }) +} + const insertImage = (view: EditorView): boolean => { const { state } = view const { from, to } = state.selection.main const selectedText = state.sliceDoc(from, to) - const alt = selectedText || '图片描述' - const insert = `![${alt}](https://)` - view.dispatch({ - changes: { from, to, insert }, - selection: { anchor: from + insert.length - 1 }, + showInputDialog('插入图片', '输入图片地址 (https://...)', '', (url) => { + if (!url) return + + const alt = selectedText || '图片描述' + const insert = `![${alt}](${url})` + + view.dispatch({ + changes: { from, to, insert }, + selection: { anchor: from + insert.length }, + }) + + view.focus() }) - view.focus() return true } @@ -156,18 +299,31 @@ const insertSpoiler = (view: EditorView): boolean => { const { state } = view const { from, to } = state.selection.main const selectedText = state.sliceDoc(from, to) - const content = selectedText || '剧透内容' - const insert = `||${content}||` - view.dispatch({ - changes: { from, to, insert }, - selection: - selectedText.length > 0 - ? { anchor: from + insert.length } - : { anchor: from + 2, head: from + 2 + content.length }, - }) + if (selectedText) { + // 如果有选中文本,直接包裹 + const insert = `||${selectedText}||` + view.dispatch({ + changes: { from, to, insert }, + selection: { anchor: from + insert.length }, + }) + view.focus() + } else { + // 否则显示输入对话框 + showInputDialog('插入剧透文本', '输入需要隐藏的内容', '', (content) => { + if (!content.trim()) return + + const insert = `||${content}||` + + view.dispatch({ + changes: { from, to, insert }, + selection: { anchor: from + insert.length }, + }) + + view.focus() + }) + } - view.focus() return true } @@ -177,41 +333,78 @@ const insertMention = const { state } = view const { from, to } = state.selection.main const selectedText = state.sliceDoc(from, to) - const handle = selectedText || 'username' - const insert = `{${platform}@${handle}}` - view.dispatch({ - changes: { from, to, insert }, - selection: - selectedText.length > 0 - ? { anchor: from + insert.length } - : { - anchor: from + platform.length + 2, - head: from + platform.length + 2 + handle.length, - }, - }) + const platformNames = { + GH: 'GitHub', + TW: 'Twitter', + TG: 'Telegram', + } + + showInputDialog( + `插入 ${platformNames[platform]} 提及`, + '输入用户名', + selectedText || '', + (handle) => { + if (!handle.trim()) return + + const insert = `{${platform}@${handle.trim()}}` + + view.dispatch({ + changes: { from, to, insert }, + selection: { anchor: from + insert.length }, + }) + + view.focus() + }, + ) - view.focus() return true } const insertContainer = - (type: string, placeholder = '在此输入内容') => + (defaultType: string, placeholder = '在此输入内容') => (view: EditorView): boolean => { const { state } = view const { from } = state.selection.main const line = state.doc.lineAt(from) const insertPos = line.to const needsNewline = line.text.length > 0 - const insert = `${needsNewline ? '\n' : ''}::: ${type}\n${placeholder}\n:::\n\n` - const cursorOffset = insert.indexOf(placeholder) - view.dispatch({ - changes: { from: insertPos, to: insertPos, insert }, - selection: { anchor: insertPos + cursorOffset }, - }) + // 如果是 banner 类型,让用户选择样式 + if (defaultType.startsWith('banner')) { + showSelectDialog( + '选择 Banner 样式', + [ + { label: 'Error (错误)', value: 'banner {error}' }, + { label: 'Warning (警告)', value: 'banner {warning}' }, + { label: 'Info (信息)', value: 'banner {info}' }, + { label: 'Success (成功)', value: 'banner {success}' }, + ], + 'banner {error}', + (type) => { + const insert = `${needsNewline ? '\n' : ''}::: ${type}\n${placeholder}\n:::\n\n` + const cursorOffset = insert.indexOf(placeholder) + + view.dispatch({ + changes: { from: insertPos, to: insertPos, insert }, + selection: { anchor: insertPos + cursorOffset }, + }) + + view.focus() + }, + ) + } else { + const insert = `${needsNewline ? '\n' : ''}::: ${defaultType}\n${placeholder}\n:::\n\n` + const cursorOffset = insert.indexOf(placeholder) + + view.dispatch({ + changes: { from: insertPos, to: insertPos, insert }, + selection: { anchor: insertPos + cursorOffset }, + }) + + view.focus() + } - view.focus() return true } @@ -221,15 +414,25 @@ const insertGallery = (view: EditorView): boolean => { const line = state.doc.lineAt(from) const insertPos = line.to const needsNewline = line.text.length > 0 - const insert = `${needsNewline ? '\n' : ''}::: gallery\nhttps://example.com/image1.jpg\nhttps://example.com/image2.jpg\n:::\n\n` - const cursorOffset = insert.indexOf('https://example.com/image1.jpg') - view.dispatch({ - changes: { from: insertPos, to: insertPos, insert }, - selection: { anchor: insertPos + cursorOffset }, - }) + showTextareaDialog( + '插入画廊', + '输入图片地址(每行一个)', + 'https://example.com/image1.jpg\nhttps://example.com/image2.jpg', + (urls) => { + if (!urls.trim()) return + + const insert = `${needsNewline ? '\n' : ''}::: gallery\n${urls.trim()}\n:::\n\n` + + view.dispatch({ + changes: { from: insertPos, to: insertPos, insert }, + selection: { anchor: insertPos + insert.length }, + }) + + view.focus() + }, + ) - view.focus() return true } @@ -239,15 +442,25 @@ const insertGrid = (view: EditorView): boolean => { const line = state.doc.lineAt(from) const insertPos = line.to const needsNewline = line.text.length > 0 - const insert = `${needsNewline ? '\n' : ''}::: grid {cols=3,gap=4}\n内容1\n\n内容2\n\n内容3\n:::\n\n` - const cursorOffset = insert.indexOf('内容1') - view.dispatch({ - changes: { from: insertPos, to: insertPos, insert }, - selection: { anchor: insertPos + cursorOffset }, - }) + showInputDialog( + '插入网格布局', + '输入配置 (如: cols=3,gap=4)', + 'cols=3,gap=4', + (config) => { + const finalConfig = config.trim() || 'cols=3,gap=4' + const insert = `${needsNewline ? '\n' : ''}::: grid {${finalConfig}}\n内容1\n\n内容2\n\n内容3\n:::\n\n` + const cursorOffset = insert.indexOf('内容1') + + view.dispatch({ + changes: { from: insertPos, to: insertPos, insert }, + selection: { anchor: insertPos + cursorOffset }, + }) + + view.focus() + }, + ) - view.focus() return true } @@ -257,18 +470,25 @@ const insertLinkCard = (view: EditorView): boolean => { const line = state.doc.lineAt(from) const insertPos = line.to const needsNewline = line.text.length > 0 - const insert = `${needsNewline ? '\n' : ''}\n\n` - const cursorOffset = insert.indexOf('username/repo') - view.dispatch({ - changes: { from: insertPos, to: insertPos, insert }, - selection: { - anchor: insertPos + cursorOffset, - head: insertPos + cursorOffset + 'username/repo'.length, + showInputDialog( + '插入链接卡片', + '输入 GitHub 仓库 (如: username/repo)', + '', + (repo) => { + if (!repo.trim()) return + + const insert = `${needsNewline ? '\n' : ''}\n\n` + + view.dispatch({ + changes: { from: insertPos, to: insertPos, insert }, + selection: { anchor: insertPos + insert.length }, + }) + + view.focus() }, - }) + ) - view.focus() return true } @@ -296,15 +516,25 @@ const insertReactComponent = (view: EditorView): boolean => { const line = state.doc.lineAt(from) const insertPos = line.to const needsNewline = line.text.length > 0 - const insert = `${needsNewline ? '\n' : ''}\`\`\`component\nimport=https://cdn.example.com/component.js\nname=Component.Name\n\`\`\`\n\n` - const cursorOffset = insert.indexOf('https://cdn.example.com/component.js') - view.dispatch({ - changes: { from: insertPos, to: insertPos, insert }, - selection: { anchor: insertPos + cursorOffset }, - }) + showTextareaDialog( + '插入 React 组件', + '输入组件配置(import 和 name)', + 'import=https://cdn.example.com/component.js\nname=Component.Name', + (config) => { + if (!config.trim()) return + + const insert = `${needsNewline ? '\n' : ''}\`\`\`component\n${config.trim()}\n\`\`\`\n\n` + + view.dispatch({ + changes: { from: insertPos, to: insertPos, insert }, + selection: { anchor: insertPos + insert.length }, + }) + + view.focus() + }, + ) - view.focus() return true } @@ -349,14 +579,25 @@ const insertInsertMark = (view: EditorView): boolean => { const insertFootnote = (view: EditorView): boolean => { const { state } = view const { from, to } = state.selection.main - const insert = `[^1]` - view.dispatch({ - changes: { from, to, insert }, - selection: { anchor: from + 2, head: from + 3 }, - }) + showInputDialog( + '插入脚注', + '输入脚注标识符 (如: 1, note1)', + '1', + (identifier) => { + if (!identifier.trim()) return + + const insert = `[^${identifier.trim()}]` + + view.dispatch({ + changes: { from, to, insert }, + selection: { anchor: from + insert.length }, + }) + + view.focus() + }, + ) - view.focus() return true } From d8f58eb1eb4e31cbf66a9c0632037980218c1840 Mon Sep 17 00:00:00 2001 From: Teror Fox Date: Tue, 17 Feb 2026 12:20:02 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E4=BD=8D=E7=BD=AE=E8=AE=A1=E7=AE=97=EF=BC=8C=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E8=B6=85=E5=87=BA=E8=A7=86=E5=8F=A3=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../editor/slash-menu/slash-menu.tsx | 50 +++++++++++++++++++ .../editor/slash-menu/use-slash-menu.ts | 30 +++++++++-- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/components/editor/slash-menu/slash-menu.tsx b/src/components/editor/slash-menu/slash-menu.tsx index e34195d2d..adb9982ed 100644 --- a/src/components/editor/slash-menu/slash-menu.tsx +++ b/src/components/editor/slash-menu/slash-menu.tsx @@ -27,6 +27,7 @@ export const SlashMenu = defineComponent({ setup(props) { const menuRef = ref(null) const itemRefs = ref>(new Map()) + const adjustedPosition = ref(false) const { isOpen, position, @@ -40,6 +41,55 @@ export const SlashMenu = defineComponent({ syncFromEditor, } = useSlashMenu(toRef(props, 'editorView')) + // 监听弹窗显示后调整位置 + watch([() => isOpen.value, () => position.value], ([visible, pos]) => { + if (visible && pos && !adjustedPosition.value) { + nextTick(() => { + if (!menuRef.value || !position.value) return + + const menuRect = menuRef.value.getBoundingClientRect() + const viewportHeight = window.innerHeight + const viewportWidth = window.innerWidth + const padding = 8 + + let needsUpdate = false + let newX = position.value.x + let newY = position.value.y + + // 检查右边界 + if (menuRect.right > viewportWidth - padding) { + newX = viewportWidth - menuRect.width - padding + needsUpdate = true + } + + // 检查底部边界 + if (menuRect.bottom > viewportHeight - padding) { + newY = viewportHeight - menuRect.height - padding + needsUpdate = true + } + + // 检查顶部边界 + if (newY < padding) { + newY = padding + needsUpdate = true + } + + // 检查左边界 + if (newX < padding) { + newX = padding + needsUpdate = true + } + + if (needsUpdate) { + adjustedPosition.value = true + position.value = { x: newX, y: newY } + } + }) + } else if (!visible) { + adjustedPosition.value = false + } + }) + const handleDocumentPointerDown = (event: PointerEvent) => { if (!isOpen.value) return const target = event.target diff --git a/src/components/editor/slash-menu/use-slash-menu.ts b/src/components/editor/slash-menu/use-slash-menu.ts index bb28e4703..40bce5e3f 100644 --- a/src/components/editor/slash-menu/use-slash-menu.ts +++ b/src/components/editor/slash-menu/use-slash-menu.ts @@ -112,11 +112,33 @@ export function useSlashMenu(editorView: Ref) { return } - isOpen.value = true - position.value = { - x: coords.left, - y: coords.bottom + 4, + // 计算弹窗位置,避免超出视口 + const menuHeight = 380 // max-h-[380px] + const menuWidth = 320 // max-w-[320px] + const padding = 8 + const viewportHeight = window.innerHeight + const viewportWidth = window.innerWidth + + // 默认显示在光标下方 + let x = coords.left + let y = coords.bottom + 4 + + // 检查是否超出右边界,如果超出则靠右对齐 + if (x + menuWidth > viewportWidth - padding) { + x = viewportWidth - menuWidth - padding + } + + // 检查是否超出底部,如果超出则显示在光标上方 + if (y + menuHeight > viewportHeight - padding) { + y = coords.top - menuHeight - 4 + // 如果上方空间也不够,则尽量靠上显示 + if (y < padding) { + y = padding + } } + + isOpen.value = true + position.value = { x, y } query.value = state.query } From 54f2c9fef058ccad4addf41289877c06721c6053 Mon Sep 17 00:00:00 2001 From: Teror Fox Date: Tue, 17 Feb 2026 12:43:30 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=80=91?= =?UTF-8?q?=E5=B8=83=E6=B5=81=E5=B8=83=E5=B1=80=E3=80=81=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E9=A1=B5=E5=92=8C=20Mermaid=20=E5=9B=BE=E8=A1=A8=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../editor/slash-menu/slash-menu-items.ts | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/src/components/editor/slash-menu/slash-menu-items.ts b/src/components/editor/slash-menu/slash-menu-items.ts index 3fbb40c07..9535fcce1 100644 --- a/src/components/editor/slash-menu/slash-menu-items.ts +++ b/src/components/editor/slash-menu/slash-menu-items.ts @@ -6,6 +6,7 @@ import { Code, Code2, Flag, + GitBranch, Grid3x3, Hash, Heading1, @@ -19,6 +20,7 @@ import { Images, Info, Italic, + Layers, Lightbulb, Link, Link2, @@ -35,7 +37,9 @@ import { Sparkles, Strikethrough, Table, + Tabs, User, + Workflow, Zap, } from 'lucide-vue-next' import { NButton, NInput, NSelect } from 'naive-ui' @@ -601,6 +605,112 @@ const insertFootnote = (view: EditorView): boolean => { return true } +const insertMasonry = (view: EditorView): boolean => { + const { state } = view + const { from } = state.selection.main + const line = state.doc.lineAt(from) + const insertPos = line.to + const needsNewline = line.text.length > 0 + + showTextareaDialog( + '插入瀑布流布局', + '输入图片地址(每行一个)', + 'https://example.com/image1.jpg\nhttps://example.com/image2.jpg', + (urls) => { + if (!urls.trim()) return + + const insert = `${needsNewline ? '\n' : ''}::: masonry\n${urls.trim()}\n:::\n\n` + + view.dispatch({ + changes: { from: insertPos, to: insertPos, insert }, + selection: { anchor: insertPos + insert.length }, + }) + + view.focus() + }, + ) + + return true +} + +const insertTabs = (view: EditorView): boolean => { + const { state } = view + const { from } = state.selection.main + const line = state.doc.lineAt(from) + const insertPos = line.to + const needsNewline = line.text.length > 0 + const insert = `${needsNewline ? '\n' : ''}\n\n\n内容1\n\n\n\n\n内容2\n\n\n\n\n` + const cursorOffset = insert.indexOf('标签1') + + view.dispatch({ + changes: { from: insertPos, to: insertPos, insert }, + selection: { + anchor: insertPos + cursorOffset, + head: insertPos + cursorOffset + 3, + }, + }) + + view.focus() + return true +} + +const insertMermaidFlowchart = (view: EditorView): boolean => { + const { state } = view + const { from } = state.selection.main + const line = state.doc.lineAt(from) + const insertPos = line.to + const needsNewline = line.text.length > 0 + const insert = `${needsNewline ? '\n' : ''}\`\`\`mermaid\nflowchart TD\n A[开始] --> B{判断}\n B -->|是| C[结束]\n B -->|否| D[继续]\n\`\`\`\n\n` + const cursorOffset = insert.indexOf('A[开始]') + + view.dispatch({ + changes: { from: insertPos, to: insertPos, insert }, + selection: { anchor: insertPos + cursorOffset }, + }) + + view.focus() + return true +} + +const insertMermaidSequence = (view: EditorView): boolean => { + const { state } = view + const { from } = state.selection.main + const line = state.doc.lineAt(from) + const insertPos = line.to + const needsNewline = line.text.length > 0 + const insert = `${needsNewline ? '\n' : ''}\`\`\`mermaid\nsequenceDiagram\n participant A\n participant B\n A->>B: 请求\n B->>A: 响应\n\`\`\`\n\n` + const cursorOffset = insert.indexOf('participant A') + + view.dispatch({ + changes: { from: insertPos, to: insertPos, insert }, + selection: { anchor: insertPos + cursorOffset }, + }) + + view.focus() + return true +} + +const insertDefinitionList = (view: EditorView): boolean => { + const { state } = view + const { from } = state.selection.main + const line = state.doc.lineAt(from) + const insertPos = line.to + const needsNewline = line.text.length > 0 + const insert = `${needsNewline ? '\n' : ''}术语 1\n: 定义 1\n\n术语 2\n: 定义 2\n\n` + const cursorOffset = insert.indexOf('术语 1') + + view.dispatch({ + changes: { from: insertPos, to: insertPos, insert }, + selection: { + anchor: insertPos + cursorOffset, + head: insertPos + cursorOffset + 4, + }, + }) + + view.focus() + return true +} + export const slashMenuGroups: SlashMenuGroup[] = [ { id: 'heading', @@ -968,6 +1078,58 @@ export const slashMenuGroups: SlashMenuGroup[] = [ keywords: ['component', 'react', 'remote', '组件', '远程'], command: insertReactComponent, }, + { + id: 'masonry', + label: 'Masonry 瀑布流', + description: '::: masonry 瀑布流布局', + icon: Layers, + keywords: ['masonry', 'waterfall', '瀑布流', '图片'], + command: insertMasonry, + }, + { + id: 'tabs', + label: 'Tabs 标签页', + description: ' 标签页', + icon: Tabs, + keywords: ['tabs', 'tab', '标签页', '选项卡'], + command: insertTabs, + }, + ], + }, + { + id: 'mermaid', + label: 'Mermaid 图表', + items: [ + { + id: 'mermaid-flowchart', + label: 'Mermaid 流程图', + description: '```mermaid flowchart', + icon: Workflow, + keywords: ['mermaid', 'flowchart', 'diagram', '流程图', '图表'], + command: insertMermaidFlowchart, + }, + { + id: 'mermaid-sequence', + label: 'Mermaid 时序图', + description: '```mermaid sequenceDiagram', + icon: GitBranch, + keywords: ['mermaid', 'sequence', 'diagram', '时序图', '序列图'], + command: insertMermaidSequence, + }, + ], + }, + { + id: 'other', + label: '其他', + items: [ + { + id: 'definition-list', + label: '定义列表', + description: '术语与定义', + icon: List, + keywords: ['definition', 'list', 'term', '定义', '术语'], + command: insertDefinitionList, + }, ], }, ] From 1ee7991eb11e18308ef19e31439975fc2b137065 Mon Sep 17 00:00:00 2001 From: Teror Fox Date: Tue, 17 Feb 2026 12:48:05 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E6=96=9C?= =?UTF-8?q?=E6=9D=A0=E8=8F=9C=E5=8D=95=E9=A1=B9=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=A0=87=E9=A2=98=E3=80=81=E6=96=87=E6=9C=AC=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=92=8C=E6=89=A9=E5=B1=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../editor/slash-menu/slash-menu-items.ts | 401 +++++++++--------- 1 file changed, 194 insertions(+), 207 deletions(-) diff --git a/src/components/editor/slash-menu/slash-menu-items.ts b/src/components/editor/slash-menu/slash-menu-items.ts index 9535fcce1..05b5f5ad9 100644 --- a/src/components/editor/slash-menu/slash-menu-items.ts +++ b/src/components/editor/slash-menu/slash-menu-items.ts @@ -37,7 +37,6 @@ import { Sparkles, Strikethrough, Table, - Tabs, User, Workflow, Zap, @@ -713,15 +712,16 @@ const insertDefinitionList = (view: EditorView): boolean => { export const slashMenuGroups: SlashMenuGroup[] = [ { - id: 'heading', - label: '标题', + id: 'basic', + label: '基础格式', items: [ + // 标题 - 只保留常用的前3级 { id: 'heading-1', label: '标题 1', description: '大标题', icon: Heading1, - keywords: ['h1', '一级标题'], + keywords: ['h1', '一级标题', 'heading'], command: (view) => setHeadingLevel(view, 1), }, { @@ -729,7 +729,7 @@ export const slashMenuGroups: SlashMenuGroup[] = [ label: '标题 2', description: '中标题', icon: Heading2, - keywords: ['h2', '二级标题'], + keywords: ['h2', '二级标题', 'heading'], command: (view) => setHeadingLevel(view, 2), }, { @@ -737,219 +737,209 @@ export const slashMenuGroups: SlashMenuGroup[] = [ label: '标题 3', description: '小标题', icon: Heading3, - keywords: ['h3', '三级标题'], + keywords: ['h3', '三级标题', 'heading'], command: (view) => setHeadingLevel(view, 3), }, - { - id: 'heading-4', - label: '标题 4', - description: '四级标题', - icon: Heading4, - keywords: ['h4', '四级标题'], - command: (view) => setHeadingLevel(view, 4), - }, - { - id: 'heading-5', - label: '标题 5', - description: '五级标题', - icon: Heading5, - keywords: ['h5', '五级标题'], - command: (view) => setHeadingLevel(view, 5), - }, - { - id: 'heading-6', - label: '标题 6', - description: '六级标题', - icon: Heading6, - keywords: ['h6', '六级标题'], - command: (view) => setHeadingLevel(view, 6), - }, - ], - }, - { - id: 'text', - label: '文本格式', - items: [ + // 文本格式 { id: 'bold', label: '粗体', - description: '加粗文字', + description: '**加粗文字**', icon: Bold, - keywords: ['bold', 'strong'], + keywords: ['bold', 'strong', '粗体'], command: commands.bold, }, { id: 'italic', label: '斜体', - description: '倾斜文字', + description: '*倾斜文字*', icon: Italic, - keywords: ['italic', 'em'], + keywords: ['italic', 'em', '斜体'], command: commands.italic, }, { id: 'strikethrough', label: '删除线', - description: '划掉文字', + description: '~~划掉文字~~', icon: Strikethrough, - keywords: ['delete', 'strike'], + keywords: ['delete', 'strike', '删除线'], command: commands.strikethrough, }, { id: 'inline-code', label: '行内代码', - description: '内联代码片段', + description: '`代码片段`', icon: Code, - keywords: ['code', 'inline'], + keywords: ['code', 'inline', '行内代码'], command: commands.inlineCode, }, - ], - }, - { - id: 'list', - label: '列表', - items: [ + // 列表 { id: 'bullet-list', label: '无序列表', - description: '项目符号列表', + description: '- 项目符号', icon: List, - keywords: ['ul', 'bullet'], + keywords: ['ul', 'bullet', '无序列表'], command: commands.bulletList, }, { id: 'ordered-list', label: '有序列表', - description: '编号列表', + description: '1. 编号列表', icon: ListOrdered, - keywords: ['ol', 'number'], + keywords: ['ol', 'number', '有序列表'], command: commands.orderedList, }, { id: 'task-list', label: '任务列表', - description: '待办事项', + description: '- [ ] 待办事项', icon: ListChecks, - keywords: ['todo', 'task'], + keywords: ['todo', 'task', '任务', '待办'], command: commands.taskList, }, ], }, { - id: 'block', - label: '块元素', + id: 'advanced-heading', + label: '更多标题', + items: [ + { + id: 'heading-4', + label: '标题 4', + description: '#### 四级标题', + icon: Heading4, + keywords: ['h4', '四级标题', 'heading'], + command: (view) => setHeadingLevel(view, 4), + }, + { + id: 'heading-5', + label: '标题 5', + description: '##### 五级标题', + icon: Heading5, + keywords: ['h5', '五级标题', 'heading'], + command: (view) => setHeadingLevel(view, 5), + }, + { + id: 'heading-6', + label: '标题 6', + description: '###### 六级标题', + icon: Heading6, + keywords: ['h6', '六级标题', 'heading'], + command: (view) => setHeadingLevel(view, 6), + }, + ], + }, + { + id: 'block-media', + label: '块与媒体', items: [ { id: 'code-block', label: '代码块', - description: '多行代码', + description: '```code 多行代码', icon: Code2, - keywords: ['code', 'block'], + keywords: ['code', 'block', '代码块'], command: commands.codeBlock, }, { id: 'quote', label: '引用', - description: '引用文本', + description: '> 引用文本', icon: Quote, - keywords: ['blockquote', 'quote'], + keywords: ['blockquote', 'quote', '引用'], command: commands.quote, }, - { - id: 'divider', - label: '分隔线', - description: '水平分隔', - icon: Minus, - keywords: ['hr', 'divider'], - command: commands.horizontalRule, - }, - { - id: 'details', - label: '折叠块', - description: '可折叠的内容区域', - icon: ChevronDown, - keywords: ['details', 'summary', 'collapse', 'toggle', '折叠'], - command: insertDetails, - }, - ], - }, - { - id: 'media', - label: '媒体与嵌入', - items: [ { id: 'link', label: '链接', - description: '添加超链接', + description: '[文字](url)', icon: Link, - keywords: ['link', 'url'], + keywords: ['link', 'url', '链接'], command: commands.link, }, { id: 'image', label: '图片', - description: '插入图片', + description: '![描述](url)', icon: Image, - keywords: ['image', 'img'], + keywords: ['image', 'img', '图片'], command: insertImage, }, { id: 'table', label: '表格', - description: '插入表格', + description: '| 列1 | 列2 |', icon: Table, - keywords: ['table', 'grid'], + keywords: ['table', 'grid', '表格'], command: insertTable, }, + { + id: 'divider', + label: '分隔线', + description: '--- 水平分隔', + icon: Minus, + keywords: ['hr', 'divider', '分隔线'], + command: commands.horizontalRule, + }, + { + id: 'details', + label: '折叠块', + description: '
折叠内容', + icon: ChevronDown, + keywords: ['details', 'summary', 'collapse', 'toggle', '折叠'], + command: insertDetails, + }, { id: 'math', label: '数学公式', - description: 'LaTeX 公式', + description: '$$ LaTeX 公式', icon: Sigma, - keywords: ['math', 'formula', 'latex'], + keywords: ['math', 'formula', 'latex', '数学', '公式'], command: insertMathBlock, }, ], }, { id: 'shiroi-alerts', - label: 'Shiroi 提示块', + label: '提示块', items: [ { id: 'alert-note', - label: 'Note 提示', - description: '>[!NOTE] 提示信息', + label: 'Note', + description: '> [!NOTE] 提示信息', icon: Info, keywords: ['note', 'info', 'alert', '提示'], command: insertAlert('NOTE'), }, { id: 'alert-tip', - label: 'Tip 建议', - description: '>[!TIP] 有用的建议', + label: 'Tip', + description: '> [!TIP] 有用建议', icon: Lightbulb, - keywords: ['tip', 'hint', 'alert', '建议', '提示'], + keywords: ['tip', 'hint', 'alert', '建议'], command: insertAlert('TIP'), }, { id: 'alert-important', - label: 'Important 重要', - description: '>[!IMPORTANT] 重要信息', + label: 'Important', + description: '> [!IMPORTANT] 重要', icon: AlertCircle, keywords: ['important', 'alert', '重要'], command: insertAlert('IMPORTANT'), }, { id: 'alert-warning', - label: 'Warning 警告', - description: '>[!WARNING] 警告信息', + label: 'Warning', + description: '> [!WARNING] 警告', icon: AlertTriangle, keywords: ['warning', 'alert', '警告'], command: insertAlert('WARNING'), }, { id: 'alert-caution', - label: 'Caution 注意', - description: '>[!CAUTION] 需要注意', + label: 'Caution', + description: '> [!CAUTION] 危险', icon: ShieldAlert, keywords: ['caution', 'danger', 'alert', '注意', '危险'], command: insertAlert('CAUTION'), @@ -957,85 +947,41 @@ export const slashMenuGroups: SlashMenuGroup[] = [ ], }, { - id: 'shiroi-inline', - label: 'Shiroi 行内语法', + id: 'shiroi-extensions', + label: 'Shiroi 扩展', items: [ - { - id: 'spoiler', - label: '剧透文本', - description: '||隐藏的内容||', - icon: MessageSquare, - keywords: ['spoiler', 'hidden', '剧透', '隐藏'], - command: insertSpoiler, - }, - { - id: 'mention-github', - label: 'GitHub 提及', - description: '{GH@username}', - icon: User, - keywords: ['mention', 'github', 'gh', '@', '提及'], - command: insertMention('GH'), - }, - { - id: 'mention-twitter', - label: 'Twitter 提及', - description: '{TW@username}', - icon: User, - keywords: ['mention', 'twitter', 'tw', '@', '提及'], - command: insertMention('TW'), - }, - { - id: 'mention-telegram', - label: 'Telegram 提及', - description: '{TG@username}', - icon: User, - keywords: ['mention', 'telegram', 'tg', '@', '提及'], - command: insertMention('TG'), - }, + // 常用扩展 { id: 'mark', label: '高亮标记', description: '==高亮文本==', icon: Highlighter, - keywords: ['mark', 'highlight', '高亮', '标记'], + keywords: ['mark', 'highlight', '高亮'], command: insertMark, }, { - id: 'insert', - label: '插入标记', - description: '++插入文本++', - icon: Plus, - keywords: ['insert', 'add', '插入', '添加'], - command: insertInsertMark, + id: 'spoiler', + label: '剧透文本', + description: '||隐藏内容||', + icon: MessageSquare, + keywords: ['spoiler', 'hidden', '剧透', '隐藏'], + command: insertSpoiler, }, { id: 'footnote', label: '脚注', - description: '[^1]', + description: '[^1] 脚注引用', icon: Hash, - keywords: ['footnote', 'reference', '脚注', '引用'], + keywords: ['footnote', 'reference', '脚注'], command: insertFootnote, }, - ], - }, - { - id: 'shiroi-advanced', - label: 'Shiroi 高级块', - items: [ - { - id: 'container-warning', - label: 'Container 警告', - description: '::: warning', - icon: Flag, - keywords: ['container', 'warning', '容器', '警告'], - command: insertContainer('warning'), - }, + // 容器 { id: 'container-banner', label: 'Banner 横幅', - description: '::: banner {error}', + description: '::: banner 提示横幅', icon: Zap, - keywords: ['banner', 'container', '横幅', '容器'], + keywords: ['banner', 'container', '横幅'], command: insertContainer('banner {error}'), }, { @@ -1043,7 +989,7 @@ export const slashMenuGroups: SlashMenuGroup[] = [ label: 'Gallery 画廊', description: '::: gallery 图片集', icon: Images, - keywords: ['gallery', 'images', '画廊', '图片集'], + keywords: ['gallery', 'images', '画廊'], command: insertGallery, }, { @@ -1051,87 +997,128 @@ export const slashMenuGroups: SlashMenuGroup[] = [ label: 'Grid 网格', description: '::: grid 网格布局', icon: Grid3x3, - keywords: ['grid', 'layout', '网格', '布局'], + keywords: ['grid', 'layout', '网格'], command: insertGrid, }, + { + id: 'masonry', + label: 'Masonry 瀑布流', + description: '::: masonry 瀑布流', + icon: Layers, + keywords: ['masonry', 'waterfall', '瀑布流'], + command: insertMasonry, + }, + { + id: 'tabs', + label: 'Tabs 标签页', + description: ' 选项卡', + icon: ChevronDown, + keywords: ['tabs', 'tab', '标签页'], + command: insertTabs, + }, + // 组件 { id: 'linkcard', - label: 'LinkCard 链接卡片', - description: ' 卡片', + label: 'LinkCard 卡片', + description: ' 链接卡片', icon: Link2, - keywords: ['linkcard', 'card', 'link', '链接卡片', '卡片'], + keywords: ['linkcard', 'card', '卡片'], command: insertLinkCard, }, { id: 'excalidraw', label: 'Excalidraw 手绘', - description: '```excalidraw 手绘图', + description: '```excalidraw 画板', icon: Paintbrush, - keywords: ['excalidraw', 'draw', 'whiteboard', '手绘', '画板'], + keywords: ['excalidraw', 'draw', '手绘'], command: insertExcalidraw, }, + ], + }, + { + id: 'shiroi-more', + label: '更多扩展', + items: [ + { + id: 'insert', + label: '插入标记', + description: '++插入文本++', + icon: Plus, + keywords: ['insert', 'add', '插入'], + command: insertInsertMark, + }, + { + id: 'mention-github', + label: 'GitHub 提及', + description: '{GH@username}', + icon: User, + keywords: ['mention', 'github', 'gh', '@'], + command: insertMention('GH'), + }, + { + id: 'mention-twitter', + label: 'Twitter 提及', + description: '{TW@username}', + icon: User, + keywords: ['mention', 'twitter', 'tw', '@'], + command: insertMention('TW'), + }, + { + id: 'mention-telegram', + label: 'Telegram 提及', + description: '{TG@username}', + icon: User, + keywords: ['mention', 'telegram', 'tg', '@'], + command: insertMention('TG'), + }, + { + id: 'container-warning', + label: 'Container 警告', + description: '::: warning 警告容器', + icon: Flag, + keywords: ['container', 'warning', '容器'], + command: insertContainer('warning'), + }, { id: 'react-component', label: 'React 组件', description: '```component 远程组件', icon: Sparkles, - keywords: ['component', 'react', 'remote', '组件', '远程'], + keywords: ['component', 'react', '组件'], command: insertReactComponent, }, { - id: 'masonry', - label: 'Masonry 瀑布流', - description: '::: masonry 瀑布流布局', - icon: Layers, - keywords: ['masonry', 'waterfall', '瀑布流', '图片'], - command: insertMasonry, - }, - { - id: 'tabs', - label: 'Tabs 标签页', - description: ' 标签页', - icon: Tabs, - keywords: ['tabs', 'tab', '标签页', '选项卡'], - command: insertTabs, + id: 'definition-list', + label: '定义列表', + description: '术语 : 定义', + icon: List, + keywords: ['definition', 'list', '定义'], + command: insertDefinitionList, }, ], }, { - id: 'mermaid', - label: 'Mermaid 图表', + id: 'diagrams', + label: '图表', items: [ { id: 'mermaid-flowchart', - label: 'Mermaid 流程图', - description: '```mermaid flowchart', + label: '流程图', + description: 'Mermaid 流程图', icon: Workflow, - keywords: ['mermaid', 'flowchart', 'diagram', '流程图', '图表'], + keywords: ['mermaid', 'flowchart', 'diagram', '流程图'], command: insertMermaidFlowchart, }, { id: 'mermaid-sequence', - label: 'Mermaid 时序图', - description: '```mermaid sequenceDiagram', + label: '时序图', + description: 'Mermaid 序列图', icon: GitBranch, - keywords: ['mermaid', 'sequence', 'diagram', '时序图', '序列图'], + keywords: ['mermaid', 'sequence', 'diagram', '时序图'], command: insertMermaidSequence, }, ], }, - { - id: 'other', - label: '其他', - items: [ - { - id: 'definition-list', - label: '定义列表', - description: '术语与定义', - icon: List, - keywords: ['definition', 'list', 'term', '定义', '术语'], - command: insertDefinitionList, - }, - ], - }, ] export interface SlashMenuItemWithGroup extends SlashMenuItem {