Files

1 line
34 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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"}}}}