1 line
34 KiB
JSON
1 line
34 KiB
JSON
{"repo": "aiziyou918/cute-markdown", "files": {"js/app-logic.js": {"60f36a2709eb7779d52e5791fcdbd3baa48d1053": {"t": 1785768938.0793848, "n": 31724, "h": "f4eb07794d2bb1f8", "c": "/**\n * CuteMD - Markdown 编辑器\n * 主应用脚本\n */\n\ndocument.addEventListener('DOMContentLoaded', () => {\n // 初始内容\n const initialValue = `# CuteMD - 一款简洁优雅的 Markdown 编辑器\n\n## 欢迎使用!\n\n这是一个基于 Vditor 实现的在线 Markdown 编辑器,旨在提供一个专注、高效且美观的写作环境。\n\n### 主要特性\n\n- **亮/暗**双主题切换\n- 动态文档**大纲**\n- 支持丰富的 Markdown 语法\n- 代码块高亮\n- 数学公式 $E=mc^2$\n- ✨ **AI 智能续写**和**智能摘要**\n- ... 以及更多!\n\n### 如何使用\n\n1. 使用顶部的工具栏进行格式化操作。\n2. 点击 **新建**、**打开** 或 **保存** 来管理您的文件。\n3. 试试点击 **✨ 智能续写** 和 **✨ 智能摘要** 按钮来体验 AI 的魔力!\n\n---\n\n> \"代码和文字一样,都是思想的表达。\"\n\n祝您使用愉快!\n`;\n \n // 初始化 Vditor 编辑器\n const vditor = new Vditor('vditor', {\n height: '100%',\n mode: 'ir',\n theme: 'classic',\n value: initialValue,\n toolbar: [\n 'emoji', 'headings', 'bold', 'italic', 'strike', 'link', '|',\n 'list', 'ordered-list', 'check', 'outdent', 'indent', '|',\n 'quote', 'line', 'code', 'inline-code', 'insert-before', 'insert-after', '|',\n 'table', 'undo', 'redo', '|',\n 'edit-mode', 'content-theme', 'code-theme', 'export',\n {\n name: 'more',\n toolbar: [\n 'fullscreen',\n 'both',\n 'preview',\n 'info',\n 'help',\n ],\n },\n ],\n after: () => {\n updateOutline();\n // 根据系统偏好设置初始主题\n const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;\n applyTheme(prefersDark ? 'dark' : 'light');\n \n // 监听全屏事件\n setupFullscreenHandler();\n },\n input: () => {\n updateOutline();\n },\n blur: () => {\n updateOutline();\n }\n });\n \n // DOM 元素引用\n const fileNameEl = document.getElementById('fileName');\n const outlineContent = document.getElementById('outlineContent');\n const themeToggleBtn = document.getElementById('themeToggleBtn');\n const sunIcon = themeToggleBtn.querySelector('.sun-icon');\n const moonIcon = themeToggleBtn.querySelector('.moon-icon');\n const outlineSidebar = document.getElementById('outlineSidebar');\n const outlineToggleBtn = document.getElementById('outlineToggleBtn');\n const outlineCloseBtn = document.getElementById('outlineCloseBtn');\n \n const newFileBtn = document.getElementById('newFileBtn');\n const openFileBtn = document.getElementById('openFileBtn');\n const saveFileBtn = document.getElementById('saveFileBtn');\n const fileInput = document.getElementById('fileInput');\n \n const continueBtn = document.getElementById('continueBtn');\n const summarizeBtn = document.getElementById('summarizeBtn');\n \n const aiModal = document.getElementById('aiModal');\n const aiModalTitle = document.getElementById('aiModalTitle');\n const aiModalContent = document.getElementById('aiModalContent');\n const aiModalCloseBtn = document.getElementById('aiModalCloseBtn');\n \n const aiSettingsBtn = document.getElementById('aiSettingsBtn');\n const aiSettingsModal = document.getElementById('aiSettingsModal');\n const aiSettingsCloseBtn = document.getElementById('aiSettingsCloseBtn');\n const saveAISettings = document.getElementById('saveAISettings');\n const clearAISettings = document.getElementById('clearAISettings');\n\n // 当前文件名(用于标题更新)\n let currentFileName = '未命名.md';\n\n /**\n * 更新浏览器标签页标题\n */\n function updateDocumentTitle(fileName) {\n document.title = `${fileName} - CuteMD`;\n }\n\n /**\n * 设置文件名\n */\n function setFileName(name) {\n currentFileName = name;\n fileNameEl.textContent = name;\n updateDocumentTitle(name);\n }\n\n /**\n * 文件名点击编辑功能\n */\n fileNameEl.addEventListener('click', () => {\n const input = document.createElement('input');\n input.type = 'text';\n input.className = 'file-name-input';\n input.value = currentFileName;\n \n // 替换元素\n fileNameEl.parentNode.replaceChild(input, fileNameEl);\n input.focus();\n input.select();\n \n // 处理输入完成\n const finishEditing = () => {\n let newName = input.value.trim();\n if (!newName) {\n newName = '未命名.md';\n } else if (!newName.endsWith('.md') && !newName.endsWith('.markdown')) {\n newName += '.md';\n }\n \n setFileName(newName);\n input.parentNode.replaceChild(fileNameEl, input);\n };\n \n input.addEventListener('blur', finishEditing);\n input.addEventListener('keydown', (e) => {\n if (e.key === 'Enter') {\n finishEditing();\n } else if (e.key === 'Escape') {\n input.value = currentFileName;\n finishEditing();\n }\n });\n });\n\n /**\n * 更新文档大纲\n */\n function updateOutline() {\n // 获取编辑器的 Markdown 内容\n const content = vditor.getValue();\n if (!content) {\n outlineContent.innerHTML = '<div class=\"outline-empty\">暂无大纲</div>';\n return;\n }\n\n // 解析 Markdown 中的标题\n const lines = content.split('\\n');\n const headings = [];\n \n lines.forEach((line, index) => {\n // 匹配标题(# 开头)\n const match = line.match(/^(#{1,6})\\s+(.+)$/);\n if (match) {\n const level = match[1].length;\n const text = match[2].trim();\n headings.push({\n level: level,\n text: text,\n lineIndex: index\n });\n }\n });\n\n // 如果没有标题,显示空状态\n if (headings.length === 0) {\n outlineContent.innerHTML = '<div class=\"outline-empty\">暂无大纲</div>';\n return;\n }\n\n // 生成大纲 HTML\n outlineContent.innerHTML = '';\n headings.forEach((heading, index) => {\n const item = document.createElement('div');\n item.className = 'outline-item';\n item.setAttribute('data-level', heading.level);\n item.textContent = heading.text;\n item.setAttribute('data-line', heading.lineIndex);\n \n // 添加点击事件\n item.addEventListener('click', () => {\n // 移除所有 active 类\n outlineContent.querySelectorAll('.outline-item').forEach(el => {\n el.classList.remove('active');\n });\n // 添加 active 类到当前项\n item.classList.add('active');\n \n // 滚动到对应位置\n const editor = vditor.vditor.ir.element;\n const allHeadings = editor.querySelectorAll('h1, h2, h3, h4, h5, h6');\n if (allHeadings[index]) {\n allHeadings[index].scrollIntoView({\n behavior: 'smooth',\n block: 'start'\n });\n }\n });\n \n outlineContent.appendChild(item);\n });\n }\n\n /**\n * 应用主题\n * @param {string} theme - 'light' 或 'dark'\n */\n function applyTheme(theme) {\n if (theme === 'dark') {\n document.body.classList.add('dark-theme');\n vditor.setTheme('dark');\n sunIcon.style.display = 'none';\n moonIcon.style.display = 'inline-block';\n } else {\n document.body.classList.remove('dark-theme');\n vditor.setTheme('classic');\n sunIcon.style.display = 'inline-block';\n moonIcon.style.display = 'none';\n }\n }\n \n /**\n * 设置全屏处理程序 - 修复全屏退出问题\n */\n function setupFullscreenHandler() {\n // 监听全屏变化事件\n document.addEventListener('fullscreenchange', handleFullscreenChange);\n document.addEventListener('webkitfullscreenchange', handleFullscreenChange);\n document.addEventListener('mozfullscreenchange', handleFullscreenChange);\n document.addEventListener('MSFullscreenChange', handleFullscreenChange);\n }\n\n function handleFullscreenChange() {\n const isFullscreen = !!(document.fullscreenElement || \n document.webkitFullscreenElement || \n document.mozFullScreenElement || \n document.msFullscreenElement);\n \n // Vditor 会自动处理全屏状态,我们只需要确保 ESC 键可以退出\n if (!isFullscreen) {\n // 确保编辑器从全屏模式正确退出\n const vditorElement = document.getElementById('vditor');\n if (vditorElement) {\n vditorElement.classList.remove('vditor--fullscreen');\n }\n }\n }\n \n /**\n * 主题切换按钮\n */\n themeToggleBtn.addEventListener('click', () => {\n const isDark = document.body.classList.contains('dark-theme');\n applyTheme(isDark ? 'light' : 'dark');\n });\n \n /**\n * 大纲位置切换(左/右)或重新打开\n */\n outlineToggleBtn.addEventListener('click', () => {\n // 如果大纲被隐藏,则重新打开\n if (outlineSidebar.classList.contains('hidden')) {\n outlineSidebar.classList.remove('hidden');\n } else {\n // 否则切换左右位置\n outlineSidebar.classList.toggle('right');\n }\n });\n \n /**\n * 隐藏大纲\n */\n outlineCloseBtn.addEventListener('click', () => {\n outlineSidebar.classList.add('hidden');\n });\n\n /**\n * 文件操作 - 新建\n */\n newFileBtn.addEventListener('click', () => {\n if (confirm('创建新文件将清空当前内容,是否继续?')) {\n vditor.setValue('');\n setFileName('未命名.md');\n updateOutline();\n }\n });\n\n /**\n * 文件操作 - 打开\n */\n openFileBtn.addEventListener('click', () => {\n fileInput.click();\n });\n \n fileInput.addEventListener('change', (event) => {\n const file = event.target.files[0];\n if (file) {\n const reader = new FileReader();\n reader.onload = (e) => {\n vditor.setValue(e.target.result);\n setFileName(file.name);\n updateOutline();\n };\n reader.readAsText(file);\n }\n // 重置文件输入以允许再次打开同一文件\n event.target.value = '';\n });\n\n /**\n * 文件操作 - 保存\n */\n saveFileBtn.addEventListener('click', () => {\n const content = vditor.getValue();\n const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });\n const a = document.createElement('a');\n a.href = URL.createObjectURL(blob);\n a.download = currentFileName;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n });\n\n // --- AI Configuration & Integration ---\n \n /**\n * AI 配置管理\n */\n const AIConfig = {\n storageKey: 'cutemd_ai_config',\n \n // 获取配置\n get() {\n const config = localStorage.getItem(this.storageKey);\n return config ? JSON.parse(config) : {\n provider: 'gemini',\n openai: { key: '', model: 'gpt-3.5-turbo', endpoint: '' },\n anthropic: { key: '', model: 'claude-3-5-sonnet-20241022' },\n gemini: { key: '', model: 'gemini-2.0-flash-exp' },\n deepseek: { key: '', model: 'deepseek-chat' },\n custom: { endpoint: '', key: '', model: '' }\n };\n },\n \n // 保存配置\n save(config) {\n localStorage.setItem(this.storageKey, JSON.stringify(config));\n },\n \n // 清除配置\n clear() {\n localStorage.removeItem(this.storageKey);\n }\n };\n \n /**\n * 加载设置到 UI\n */\n function loadAISettingsToUI() {\n const config = AIConfig.get();\n \n // 设置提供商\n document.querySelector(`input[name=\"aiProvider\"][value=\"${config.provider}\"]`).checked = true;\n showProviderSettings(config.provider);\n \n // 加载各个提供商的设置\n document.getElementById('openaiKey').value = config.openai.key || '';\n document.getElementById('openaiModel').value = config.openai.model || 'gpt-3.5-turbo';\n document.getElementById('openaiEndpoint').value = config.openai.endpoint || '';\n \n document.getElementById('anthropicKey').value = config.anthropic.key || '';\n document.getElementById('anthropicModel').value = config.anthropic.model || 'claude-3-5-sonnet-20241022';\n \n document.getElementById('geminiKey').value = config.gemini.key || '';\n document.getElementById('geminiModel').value = config.gemini.model || 'gemini-2.0-flash-exp';\n \n document.getElementById('deepseekKey').value = config.deepseek.key || '';\n document.getElementById('deepseekModel').value = config.deepseek.model || 'deepseek-chat';\n \n document.getElementById('customEndpoint').value = config.custom.endpoint || '';\n document.getElementById('customKey').value = config.custom.key || '';\n document.getElementById('customModel').value = config.custom.model || '';\n }\n \n /**\n * 显示/隐藏提供商设置\n */\n function showProviderSettings(provider) {\n document.getElementById('openaiSettings').style.display = provider === 'openai' ? 'block' : 'none';\n document.getElementById('anthropicSettings').style.display = provider === 'anthropic' ? 'block' : 'none';\n document.getElementById('geminiSettings').style.display = provider === 'gemini' ? 'block' : 'none';\n document.getElementById('deepseekSettings').style.display = provider === 'deepseek' ? 'block' : 'none';\n document.getElementById('customSettings').style.display = provider === 'custom' ? 'block' : 'none';\n }\n \n /**\n * 打开 AI 设置\n */\n aiSettingsBtn.addEventListener('click', () => {\n loadAISettingsToUI();\n aiSettingsModal.style.display = 'flex';\n });\n \n /**\n * 关闭 AI 设置\n */\n const closeAISettingsModal = () => {\n aiSettingsModal.style.display = 'none';\n };\n \n aiSettingsCloseBtn.addEventListener('click', closeAISettingsModal);\n aiSettingsModal.addEventListener('click', (e) => {\n if (e.target === aiSettingsModal) {\n closeAISettingsModal();\n }\n });\n \n /**\n * 提供商切换\n */\n document.querySelectorAll('input[name=\"aiProvider\"]').forEach(radio => {\n radio.addEventListener('change', (e) => {\n showProviderSettings(e.target.value);\n });\n });\n \n /**\n * 保存 AI 设置\n */\n saveAISettings.addEventListener('click', () => {\n const provider = document.querySelector('input[name=\"aiProvider\"]:checked').value;\n \n const config = {\n provider,\n openai: {\n key: document.getElementById('openaiKey').value.trim(),\n model: document.getElementById('openaiModel').value,\n endpoint: document.getElementById('openaiEndpoint').value.trim()\n },\n anthropic: {\n key: document.getElementById('anthropicKey').value.trim(),\n model: document.getElementById('anthropicModel').value\n },\n gemini: {\n key: document.getElementById('geminiKey').value.trim(),\n model: document.getElementById('geminiModel').value\n },\n deepseek: {\n key: document.getElementById('deepseekKey').value.trim(),\n model: document.getElementById('deepseekModel').value\n },\n custom: {\n endpoint: document.getElementById('customEndpoint').value.trim(),\n key: document.getElementById('customKey').value.trim(),\n model: document.getElementById('customModel').value.trim()\n }\n };\n \n // 验证当前提供商是否配置了必要信息\n let valid = true;\n switch(provider) {\n case 'openai':\n if (!config.openai.key) {\n alert('请输入 OpenAI API Key');\n valid = false;\n }\n break;\n case 'anthropic':\n if (!config.anthropic.key) {\n alert('请输入 Anthropic API Key');\n valid = false;\n }\n break;\n case 'gemini':\n if (!config.gemini.key) {\n alert('请输入 Gemini API Key');\n valid = false;\n }\n break;\n case 'deepseek':\n if (!config.deepseek.key) {\n alert('请输入 DeepSeek API Key');\n valid = false;\n }\n break;\n case 'custom':\n if (!config.custom.endpoint || !config.custom.key || !config.custom.model) {\n alert('请完整填写自定义 API 配置');\n valid = false;\n }\n break;\n }\n \n if (valid) {\n AIConfig.save(config);\n alert('✅ AI 设置已保存!');\n closeAISettingsModal();\n }\n });\n \n /**\n * 清除 AI 设置\n */\n clearAISettings.addEventListener('click', () => {\n if (confirm('确定要清除所有 AI 配置吗?')) {\n AIConfig.clear();\n loadAISettingsToUI();\n alert('✅ AI 设置已清除!');\n }\n });\n \n /**\n * 统一的 AI API 调用函数\n */\n async function callAI(prompt, button) {\n const config = AIConfig.get();\n const providerConfig = config[config.provider];\n \n // 检查配置\n if (!providerConfig || !providerConfig.key) {\n alert(`❌ 请先配置 AI 服务\\n\\n当前选择的提供商: ${config.provider}\\n\\n点击顶部的设置按钮 (⚙️) 进行配置。`);\n return null;\n }\n \n const originalText = button.innerHTML;\n button.innerHTML = '<span class=\"loader\"></span>';\n button.disabled = true;\n\n try {\n let result;\n \n switch(config.provider) {\n case 'openai':\n result = await callOpenAI(prompt, providerConfig);\n break;\n case 'anthropic':\n result = await callAnthropic(prompt, providerConfig);\n break;\n case 'gemini':\n result = await callGemini(prompt, providerConfig);\n break;\n case 'deepseek':\n result = await callDeepSeek(prompt, providerConfig);\n break;\n case 'custom':\n result = await callCustomAPI(prompt, providerConfig);\n break;\n default:\n throw new Error('未知的 AI 提供商');\n }\n \n return result;\n } catch (error) {\n console.error('AI API 调用失败:', error);\n alert(`❌ AI 功能调用失败:${error.message}\\n\\n请检查:\\n1. API Key 是否正确\\n2. 网络连接是否正常\\n3. 所选服务在您的地区是否可用`);\n return null;\n } finally {\n button.innerHTML = originalText;\n button.disabled = false;\n }\n }\n \n /**\n * OpenAI API 调用\n */\n async function callOpenAI(prompt, config) {\n const endpoint = config.endpoint || 'https://api.openai.com/v1';\n const url = `${endpoint}/chat/completions`;\n \n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${config.key}`\n },\n body: JSON.stringify({\n model: config.model,\n messages: [{ role: 'user', content: prompt }],\n temperature: 0.7\n })\n });\n \n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(error.error?.message || `HTTP ${response.status}`);\n }\n \n const data = await response.json();\n return data.choices?.[0]?.message?.content || '';\n }\n \n /**\n * Anthropic Claude API 调用\n */\n async function callAnthropic(prompt, config) {\n const url = 'https://api.anthropic.com/v1/messages';\n \n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': config.key,\n 'anthropic-version': '2023-06-01'\n },\n body: JSON.stringify({\n model: config.model,\n max_tokens: 4096,\n messages: [{ role: 'user', content: prompt }]\n })\n });\n \n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(error.error?.message || `HTTP ${response.status}`);\n }\n \n const data = await response.json();\n return data.content?.[0]?.text || '';\n }\n \n /**\n * Google Gemini API 调用\n */\n async function callGemini(prompt, config) {\n const url = `https://generativelanguage.googleapis.com/v1beta/models/${config.model}:generateContent?key=${config.key}`;\n \n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n contents: [{ parts: [{ text: prompt }] }]\n })\n });\n \n if (!response.ok) {\n if (response.status === 400 || response.status === 403) {\n throw new Error('Gemini API 在您所在的地区不可用');\n }\n throw new Error(`HTTP ${response.status}`);\n }\n \n const data = await response.json();\n return data.candidates?.[0]?.content?.parts?.[0]?.text || '';\n }\n \n /**\n * DeepSeek API 调用\n */\n async function callDeepSeek(prompt, config) {\n const url = 'https://api.deepseek.com/v1/chat/completions';\n \n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${config.key}`\n },\n body: JSON.stringify({\n model: config.model,\n messages: [{ role: 'user', content: prompt }],\n temperature: 0.7\n })\n });\n \n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(error.error?.message || `HTTP ${response.status}`);\n }\n \n const data = await response.json();\n return data.choices?.[0]?.message?.content || '';\n }\n \n /**\n * 自定义 API 调用 (OpenAI 格式)\n */\n async function callCustomAPI(prompt, config) {\n const url = `${config.endpoint}/chat/completions`;\n \n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${config.key}`\n },\n body: JSON.stringify({\n model: config.model,\n messages: [{ role: 'user', content: prompt }],\n temperature: 0.7\n })\n });\n \n if (!response.ok) {\n const error = await response.json().catch(() => ({}));\n throw new Error(error.error?.message || `HTTP ${response.status}`);\n }\n \n const data = await response.json();\n return data.choices?.[0]?.message?.content || '';\n }\n\n /**\n * 智能续写功能\n */\n continueBtn.addEventListener('click', async () => {\n if (!vditor) return;\n const currentText = vditor.getValue();\n if (currentText.trim().length < 10) {\n alert(\"请输入至少 10 个字符的内容以便 AI 更好地理解上下文。\");\n return;\n }\n \n const prompt = `You are a helpful writing assistant. Please continue writing the following text in a natural and coherent way, in the same language. Do not repeat the original text in your response. Here is the text:\\n\\n---\\n\\n${currentText}`;\n \n const generatedText = await callAI(prompt, continueBtn);\n \n if (generatedText) {\n vditor.insertValue(`\\n\\n${generatedText}`);\n }\n });\n\n /**\n * 智能摘要功能\n */\n summarizeBtn.addEventListener('click', async () => {\n if (!vditor) return;\n const currentText = vditor.getValue();\n if (currentText.trim().length < 50) {\n alert(\"请输入至少 50 个字符的内容以便 AI 生成有意义的摘要。\");\n return;\n }\n \n const prompt = `You are an expert summarizer. Please provide a concise summary of the following text in a few bullet points. The summary must be in the same language as the original text. Here is the text:\\n\\n---\\n\\n${currentText}`;\n \n const summary = await callAI(prompt, summarizeBtn);\n \n if (summary) {\n // 使用 Vditor 的 markdown 解析器渲染摘要在 modal 中\n aiModalTitle.textContent = '✨ 智能摘要';\n Vditor.preview(aiModalContent, summary, {\n cdn: 'https://unpkg.com/vditor@3.10.4',\n });\n aiModal.style.display = 'flex';\n }\n });\n \n /**\n * 关闭 AI Modal\n */\n const closeAiModal = () => {\n aiModal.style.display = 'none';\n aiModalContent.innerHTML = '';\n };\n \n aiModalCloseBtn.addEventListener('click', closeAiModal);\n aiModal.addEventListener('click', (e) => {\n if (e.target === aiModal) {\n closeAiModal();\n }\n });\n\n // 键盘快捷键系统\n document.addEventListener('keydown', (e) => {\n const mod = e.ctrlKey || e.metaKey; // Ctrl on Windows/Linux, Cmd on Mac\n const shift = e.shiftKey;\n const alt = e.altKey;\n const key = e.key.toLowerCase();\n \n // ========== 文件操作 ==========\n \n // Ctrl/Cmd + N: 新建\n if (mod && !shift && !alt && key === 'n') {\n e.preventDefault();\n newFileBtn.click();\n return;\n }\n \n // Ctrl/Cmd + O: 打开\n if (mod && !shift && !alt && key === 'o') {\n e.preventDefault();\n openFileBtn.click();\n return;\n }\n \n // Ctrl/Cmd + S: 保存\n if (mod && !shift && !alt && key === 's') {\n e.preventDefault();\n saveFileBtn.click();\n return;\n }\n \n // Ctrl/Cmd + W: 关闭(新建空白文档)\n if (mod && !shift && !alt && key === 'w') {\n e.preventDefault();\n newFileBtn.click();\n return;\n }\n \n // ========== 视图操作 ==========\n \n // Ctrl/Cmd + Shift + L: 切换侧边栏/大纲\n if (mod && shift && !alt && key === 'l') {\n e.preventDefault();\n outlineToggleBtn.click();\n return;\n }\n \n // Ctrl/Cmd + Shift + 1: 大纲(与切换侧边栏相同)\n if (mod && shift && !alt && key === '1') {\n e.preventDefault();\n outlineToggleBtn.click();\n return;\n }\n \n // Ctrl/Cmd + /: 源代码模式(Vditor 自带,不需要处理)\n // F8: 对焦模式(需要通过 Vditor API)\n if (!mod && !shift && !alt && e.key === 'F8') {\n e.preventDefault();\n // 切换对焦模式 - 通过 Vditor 内置功能\n const focusBtn = document.querySelector('[data-type=\"focus\"]');\n if (focusBtn) focusBtn.click();\n return;\n }\n \n // F9: 打字机模式\n if (!mod && !shift && !alt && e.key === 'F9') {\n e.preventDefault();\n // 切换打字机模式\n const typewriterBtn = document.querySelector('[data-type=\"typewriter\"]');\n if (typewriterBtn) typewriterBtn.click();\n return;\n }\n \n // F11: 全屏模式\n if (!mod && !shift && !alt && e.key === 'F11') {\n e.preventDefault();\n const fullscreenBtn = document.querySelector('[data-type=\"fullscreen\"]');\n if (fullscreenBtn) fullscreenBtn.click();\n return;\n }\n \n // ========== 编辑操作 ==========\n // 注意:大部分编辑操作(如 Ctrl+B, Ctrl+I 等)由 Vditor 内置处理\n // 这里只处理一些特殊的或者需要自定义的快捷键\n \n // Ctrl/Cmd + F: 查找(Vditor 自带)\n // Ctrl/Cmd + H: 替换(Vditor 自带)\n \n // ========== 段落操作 ==========\n \n // Ctrl/Cmd + 0: 段落\n if (mod && !shift && !alt && key === '0') {\n e.preventDefault();\n const paragraphBtn = document.querySelector('[data-type=\"paragraph\"]');\n if (paragraphBtn) paragraphBtn.click();\n return;\n }\n \n // Ctrl/Cmd + 1-6: 标题 1-6\n if (mod && !shift && !alt && ['1', '2', '3', '4', '5', '6'].includes(key)) {\n e.preventDefault();\n const headingBtn = document.querySelector(`[data-type=\"heading\"][data-level=\"${key}\"]`);\n if (headingBtn) {\n headingBtn.click();\n } else {\n // 如果找不到按钮,尝试插入标题\n const level = parseInt(key);\n const hashes = '#'.repeat(level);\n vditor.insertValue(`\\n${hashes} `);\n }\n return;\n }\n \n // Ctrl/Cmd + Shift + K: 代码块\n if (mod && shift && !alt && key === 'k') {\n e.preventDefault();\n const codeBtn = document.querySelector('[data-type=\"code\"]');\n if (codeBtn) codeBtn.click();\n return;\n }\n \n // Ctrl/Cmd + Shift + Q: 引用\n if (mod && shift && !alt && key === 'q') {\n e.preventDefault();\n const quoteBtn = document.querySelector('[data-type=\"quote\"]');\n if (quoteBtn) quoteBtn.click();\n return;\n }\n \n // Ctrl/Cmd + Shift + [: 有序列表\n if (mod && shift && !alt && key === '[') {\n e.preventDefault();\n const orderedListBtn = document.querySelector('[data-type=\"ordered-list\"]');\n if (orderedListBtn) orderedListBtn.click();\n return;\n }\n \n // Ctrl/Cmd + Shift + ]: 无序列表\n if (mod && shift && !alt && key === ']') {\n e.preventDefault();\n const listBtn = document.querySelector('[data-type=\"list\"]');\n if (listBtn) listBtn.click();\n return;\n }\n \n // Ctrl/Cmd + T: 表格\n if (mod && !shift && !alt && key === 't') {\n e.preventDefault();\n const tableBtn = document.querySelector('[data-type=\"table\"]');\n if (tableBtn) tableBtn.click();\n return;\n }\n \n // ========== 格式化操作 ==========\n // Vditor 内置支持的快捷键:\n // Ctrl+B: 粗体\n // Ctrl+I: 斜体\n // Ctrl+U: 下划线\n // Ctrl+K: 链接\n // 等等...\n \n // Ctrl/Cmd + Shift + `: 行内代码\n if (mod && shift && !alt && key === '`') {\n e.preventDefault();\n const inlineCodeBtn = document.querySelector('[data-type=\"inline-code\"]');\n if (inlineCodeBtn) inlineCodeBtn.click();\n return;\n }\n \n // Ctrl/Cmd + Shift + I: 图片\n if (mod && shift && !alt && key === 'i') {\n e.preventDefault();\n const uploadBtn = document.querySelector('[data-type=\"upload\"]');\n if (uploadBtn) uploadBtn.click();\n return;\n }\n \n // Ctrl/Cmd + \\: 清除格式\n if (mod && !shift && !alt && key === '\\\\') {\n e.preventDefault();\n const clearBtn = document.querySelector('[data-type=\"clearCache\"]');\n if (clearBtn) clearBtn.click();\n return;\n }\n });\n\n // 初始化文件名和标题\n setFileName(currentFileName);\n});\n"}}}} |