From 60a8950f36c1007a2e0c0da86085ece2de8c541d Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Mon, 29 Jun 2026 13:22:09 +0800 Subject: [PATCH 1/5] =?UTF-8?q?refactor(app):=20=E5=85=A8=E5=B1=80?= =?UTF-8?q?=E5=BF=AB=E6=8D=B7=E9=94=AE=E5=A4=84=E7=90=86=E6=8A=BD=E7=A6=BB?= =?UTF-8?q?=E4=B8=BA=20useGlobalShortcuts=20composable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 捕获阶段 keydown 拦截与派发、监听生命周期移入独立 composable; App 传入 matchShortcut/shortcutDispatch/isOverlayOpen。零行为变化。 --- src/App.vue | 17 +++-------------- src/composables/useGlobalShortcuts.ts | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 src/composables/useGlobalShortcuts.ts diff --git a/src/App.vue b/src/App.vue index 834ee47c..28bcb68f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -540,6 +540,7 @@ import {useGitStatus} from './composables/useGitStatus' import {useSessionTabs} from './composables/useSessionTabs' import {useEditorContextMenu} from './composables/useEditorContextMenu' import {useRunConfig} from './composables/useRunConfig' +import {useGlobalShortcuts} from './composables/useGlobalShortcuts' import EditorTabs from './components/EditorTabs.vue' import IndentControl from './components/IndentControl.vue' import Sidebar from './components/Sidebar.vue' @@ -2200,18 +2201,8 @@ const paletteCommands = computed(() => [ {id: 'settings', label: t('command.settings'), icon: SettingsIcon, run: () => { showSettings.value = true }} ]) -const onGlobalKeydown = (e: KeyboardEvent) => { - if (isOverlayOpen()) { - return - } - const action = matchShortcut(e) - if (action && shortcutDispatch[action]) { - // 捕获阶段拦截:阻止事件到达编辑器(避免 Cmd+Enter 等被插入换行) - e.preventDefault() - e.stopPropagation() - shortcutDispatch[action]() - } -} +// 全局快捷键(捕获拦截 + 派发)抽离到 useGlobalShortcuts +useGlobalShortcuts(matchShortcut, shortcutDispatch, isOverlayOpen) const {init: initTheme, setTheme: setAppTheme} = useTheme() @@ -2236,7 +2227,6 @@ onMounted(async () => { // 恢复上次打开的文件标签 await restoreSession() - window.addEventListener('keydown', onGlobalKeydown, true) window.addEventListener('lsp:open-location', onLspOpenLocation) window.addEventListener('lsp:code-actions', onLspCodeActions) @@ -2246,7 +2236,6 @@ onMounted(async () => { onUnmounted(() => { cleanupEventListeners() - window.removeEventListener('keydown', onGlobalKeydown, true) window.removeEventListener('lsp:open-location', onLspOpenLocation) window.removeEventListener('lsp:code-actions', onLspCodeActions) }) diff --git a/src/composables/useGlobalShortcuts.ts b/src/composables/useGlobalShortcuts.ts new file mode 100644 index 00000000..c4124f35 --- /dev/null +++ b/src/composables/useGlobalShortcuts.ts @@ -0,0 +1,24 @@ +// 全局快捷键:捕获阶段拦截 keydown,匹配到动作则阻止默认并派发。 +import {onMounted, onUnmounted} from 'vue' + +export function useGlobalShortcuts( + matchShortcut: (e: KeyboardEvent) => string | null, + dispatch: Record void>, + isOverlayOpen: () => boolean +) { + const onGlobalKeydown = (e: KeyboardEvent) => { + if (isOverlayOpen()) { + return + } + const action = matchShortcut(e) + if (action && dispatch[action]) { + // 捕获阶段拦截:阻止事件到达编辑器(避免 Cmd+Enter 等被插入换行) + e.preventDefault() + e.stopPropagation() + dispatch[action]() + } + } + + onMounted(() => window.addEventListener('keydown', onGlobalKeydown, true)) + onUnmounted(() => window.removeEventListener('keydown', onGlobalKeydown, true)) +} From 67865f6733ca8074d2a6653bfd025c56e9c09e43 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Mon, 29 Jun 2026 13:30:41 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat(editor):=20=E6=96=B0=E5=A2=9E=E3=80=8C?= =?UTF-8?q?=E5=A4=8D=E5=88=B6=E4=B8=BA=20Markdown=20=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=9D=97=E3=80=8D=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 命令面板「文本」分组:把选区(或全文)包成带语言围栏的 Markdown 代码块复制到剪贴板, 语言取当前编辑语言并去掉尾部数字(python3→python),便于分享到 issue/聊天/文档。 --- src/App.vue | 20 ++++++++++++++++++++ src/i18n/locales/en.json | 2 ++ src/i18n/locales/zh-CN.json | 2 ++ 3 files changed, 24 insertions(+) diff --git a/src/App.vue b/src/App.vue index 28bcb68f..b6cba0c5 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1109,6 +1109,25 @@ const editorView = shallowRef(null) // ===== 文本变换命令(排序行/大小写/去重/去行尾空白)===== const {transformSelectionOrLine, sortLines, removeDuplicateLines, trimTrailingWhitespace} = useTextCommands(editorView) +// 复制为 Markdown 代码块(选区或全文,带语言围栏) +const copyAsMarkdown = async () => { + const view = editorView.value + if (!view) { + return + } + const sel = view.state.selection.main + const text = sel.empty ? view.state.doc.toString() : view.state.doc.sliceString(sel.from, sel.to) + const lang = (currentLanguage.value || '').toLowerCase().replace(/\d+$/, '') + const fence = '```' + lang + '\n' + text.replace(/\n$/, '') + '\n```' + try { + await navigator.clipboard.writeText(fence) + toast.success(t('app.copiedMarkdown')) + } + catch (error) { + toast.error(t('app.copyFailed') + error) + } +} + // AI 自然语言生成 / 选区改写 const showGenerate = ref(false) const generateSelection = ref('') @@ -2188,6 +2207,7 @@ const paletteCommands = computed(() => [ {id: 'toLowerCase', label: t('command.toLowerCase'), group: t('command.groupText'), icon: CaseLower, run: () => transformSelectionOrLine(s => s.toLowerCase())}, {id: 'removeDuplicateLines', label: t('command.removeDuplicateLines'), group: t('command.groupText'), icon: ListChecks, run: () => removeDuplicateLines()}, {id: 'trimTrailingWhitespace', label: t('command.trimTrailingWhitespace'), group: t('command.groupText'), icon: Eraser, run: () => trimTrailingWhitespace()}, + {id: 'copyAsMarkdown', label: t('command.copyAsMarkdown'), group: t('command.groupText'), icon: Code2, run: () => copyAsMarkdown()}, {id: 'toggleAutoReveal', label: t('command.toggleAutoReveal'), icon: FolderOpen, run: () => toggleAutoReveal()}, {id: 'toggleSidebar', label: t('command.toggleSidebar'), icon: PanelLeft, hint: hintOf('toggleSidebar'), run: () => toggleSidebar()}, {id: 'toggleZen', label: t('command.toggleZen'), icon: Minimize2, run: () => toggleZen()}, diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 66383903..315b8f01 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -702,6 +702,7 @@ "toLowerCase": "Transform to lowercase", "removeDuplicateLines": "Remove duplicate lines", "trimTrailingWhitespace": "Trim trailing whitespace", + "copyAsMarkdown": "Copy as Markdown code block", "groupText": "Text", "toggleAutoReveal": "Toggle: auto-reveal active file", "toggleSidebar": "Toggle sidebar", @@ -1320,6 +1321,7 @@ "untitled": "Untitled", "multiEngine": "This type has multiple run engines; selected \"{name}\", switch manually in the dropdown", "pathCopied": "Path copied", + "copiedMarkdown": "Copied as Markdown code block", "copyFailed": "Copy failed: ", "notTextFile": "Not a text file, cannot open", "openFailed": "Open failed: ", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 0b93894e..52e8964b 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -702,6 +702,7 @@ "toLowerCase": "转为小写", "removeDuplicateLines": "删除重复行", "trimTrailingWhitespace": "去除行尾空白", + "copyAsMarkdown": "复制为 Markdown 代码块", "groupText": "文本", "toggleAutoReveal": "切换:自动定位当前文件", "toggleSidebar": "切换侧栏", @@ -1320,6 +1321,7 @@ "untitled": "未命名", "multiEngine": "该类型可用多个运行引擎,已选「{name}」,可在下拉手动切换", "pathCopied": "已复制路径", + "copiedMarkdown": "已复制为 Markdown 代码块", "copyFailed": "复制失败: ", "notTextFile": "不是文本文件,无法打开", "openFailed": "打开失败: ", From 39d4234c82f15533a33e8954057959c6cd19dcbf Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Mon, 29 Jun 2026 13:54:25 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat(editor):=20=E6=96=B0=E5=A2=9E=E3=80=8C?= =?UTF-8?q?=E4=B8=8E=E5=89=AA=E8=B4=B4=E6=9D=BF=E6=AF=94=E8=BE=83=E3=80=8D?= =?UTF-8?q?=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 命令面板用现有 DiffView 把剪贴板内容(原始)与当前编辑内容(修改)做差异对比, 便于快速核对粘贴改动;剪贴板空/读取失败有提示,弹层纳入快捷键屏蔽。 --- src/App.vue | 26 +++++++++++++++++++++++++- src/i18n/locales/en.json | 4 ++++ src/i18n/locales/zh-CN.json | 4 ++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/App.vue b/src/App.vue index b6cba0c5..f78f1b5e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -367,6 +367,14 @@ :file-name="currentFileName" @close="showDiff = false"/> + + + { } showDiff.value = true } +// 与剪贴板内容比较(剪贴板为原始,当前编辑内容为修改) +const clipboardDiff = ref<{ original: string } | null>(null) +const compareWithClipboard = async () => { + try { + const text = await navigator.clipboard.readText() + if (!text) { + toast.info(t('app.clipboardEmpty')) + return + } + clipboardDiff.value = {original: text} + } + catch (error) { + toast.error(t('app.clipboardReadFailed') + error) + } +} const togglePreview = () => { showPreview.value = !showPreview.value } @@ -2116,7 +2139,7 @@ const isOverlayOpen = () => || showHistory.value || showViewer.value || showRunPrompt.value || showQuickOpen.value || showGenerate.value || showSearch.value || showCommandPalette.value || showDiff.value || showGoToLine.value || showOutline.value || showSnippets.value - || applyPreview.value != null + || applyPreview.value != null || clipboardDiff.value != null // 全局快捷键(绑定可在设置中自定义) const {matchAction: matchShortcut, reload: reloadShortcuts, getBinding, formatCombo} = useShortcuts() @@ -2191,6 +2214,7 @@ const paletteCommands = computed(() => [ {id: 'formatWithAi', label: t('command.formatWithAi'), icon: Sparkles, run: () => formatWithAi()}, {id: 'history', label: t('command.history'), icon: History, run: () => { showHistory.value = true }}, {id: 'diff', label: t('command.diff'), icon: GitCompare, run: () => openDiff()}, + {id: 'compareClipboard', label: t('command.compareClipboard'), icon: GitCompare, run: () => compareWithClipboard()}, {id: 'preview', label: t('command.preview'), icon: Eye, run: () => togglePreview()}, {id: 'git', label: t('command.git'), icon: GitBranch, run: () => openGit()}, {id: 'tasks', label: t('command.tasks'), icon: ListChecks, run: () => openTasks()}, diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 315b8f01..5f86652c 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -703,6 +703,7 @@ "removeDuplicateLines": "Remove duplicate lines", "trimTrailingWhitespace": "Trim trailing whitespace", "copyAsMarkdown": "Copy as Markdown code block", + "compareClipboard": "Compare with clipboard", "groupText": "Text", "toggleAutoReveal": "Toggle: auto-reveal active file", "toggleSidebar": "Toggle sidebar", @@ -1058,6 +1059,7 @@ }, "diff": { "title": "Diff", + "clipboardTitle": "Compare with clipboard", "close": "Close", "subtitle": "Saved (red) → Current (green)", "noDiff": "No differences, content is identical", @@ -1322,6 +1324,8 @@ "multiEngine": "This type has multiple run engines; selected \"{name}\", switch manually in the dropdown", "pathCopied": "Path copied", "copiedMarkdown": "Copied as Markdown code block", + "clipboardEmpty": "Clipboard is empty", + "clipboardReadFailed": "Failed to read clipboard: ", "copyFailed": "Copy failed: ", "notTextFile": "Not a text file, cannot open", "openFailed": "Open failed: ", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 52e8964b..34ee5c19 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -703,6 +703,7 @@ "removeDuplicateLines": "删除重复行", "trimTrailingWhitespace": "去除行尾空白", "copyAsMarkdown": "复制为 Markdown 代码块", + "compareClipboard": "与剪贴板比较", "groupText": "文本", "toggleAutoReveal": "切换:自动定位当前文件", "toggleSidebar": "切换侧栏", @@ -1058,6 +1059,7 @@ }, "diff": { "title": "差异对比", + "clipboardTitle": "与剪贴板比较", "close": "关闭", "subtitle": "已保存(红) → 当前(绿)", "noDiff": "没有差异,内容一致", @@ -1322,6 +1324,8 @@ "multiEngine": "该类型可用多个运行引擎,已选「{name}」,可在下拉手动切换", "pathCopied": "已复制路径", "copiedMarkdown": "已复制为 Markdown 代码块", + "clipboardEmpty": "剪贴板为空", + "clipboardReadFailed": "读取剪贴板失败: ", "copyFailed": "复制失败: ", "notTextFile": "不是文本文件,无法打开", "openFailed": "打开失败: ", From f3835d00c3412b071344b1574ab9403639ed55f2 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Mon, 29 Jun 2026 17:32:18 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(ci):=20docs=20=E4=BA=A7=E7=89=A9?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=94=B9=E7=94=A8=E7=BB=9D=E5=AF=B9=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=EF=BC=8C=E4=BF=AE=E5=A4=8D=20tar=20=E6=89=BE=E4=B8=8D?= =?UTF-8?q?=E5=88=B0=20docs/dist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit job 设了 working-directory: docs,upload-pages-artifact 内部 tar 继承该目录, 相对 path 'docs/dist' 被解析为 docs/docs/dist;改用 ${{ github.workspace }}/docs/dist。 --- .github/workflows/docs.yml | 4 +++- docs/package.json | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 docs/package.json diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 85b61feb..47eabfcc 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -47,7 +47,9 @@ jobs: - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: - path: docs/dist + # 用绝对路径:job 设了 working-directory: docs,upload-pages-artifact 内部 + # 的 tar 会继承该目录,相对路径 docs/dist 会被当成 docs/docs/dist 而找不到。 + path: ${{ github.workspace }}/docs/dist deploy: needs: build diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..62f6e2f2 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,30 @@ +{ + "name": "codeforge-website", + "type": "module", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite-ssg build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.5.13", + "vue-i18n": "^11.4.6", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@unhead/vue": "^1.11.14", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/tsconfig": "^0.7.0", + "autoprefixer": "^10.4.20", + "markdown-it-anchor": "^9.2.0", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", + "unplugin-vue-markdown": "^28.3.1", + "vite": "^6.0.7", + "vite-ssg": "^0.24.1", + "vue-tsc": "^2.2.0" + } +} From 0be12cd95909256d07b170f07771269c02f559a7 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Mon, 29 Jun 2026 17:35:43 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix(docs):=20=E5=B0=86=20docs/tsconfig.json?= =?UTF-8?q?=20=E7=BA=B3=E5=85=A5=E7=89=88=E6=9C=AC=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根 .gitignore 的 *.json 规则会忽略 docs 下的 JSON;在 docs/.gitignore 用 !tsconfig.json/!package.json 重新纳入,确保克隆后类型检查/构建配置完整。 --- docs/.gitignore | 6 ++++-- docs/tsconfig.json | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 docs/tsconfig.json diff --git a/docs/.gitignore b/docs/.gitignore index 9a6487f9..2cc0a112 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -3,6 +3,8 @@ dist .vite *.local -# 根 .gitignore 忽略了所有 pnpm-lock.yaml,这里为文档站重新纳入, -# 供 CI 的 --frozen-lockfile 与依赖缓存使用 +# 根 .gitignore 用 *.json / pnpm-lock.yaml 忽略了这些文件, +# 这里为文档站重新纳入版本控制(构建/类型检查/依赖锁定需要) !pnpm-lock.yaml +!tsconfig.json +!package.json diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 00000000..58e799cf --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "baseUrl": ".", + "paths": {"@/*": ["./src/*"]}, + "types": ["vite/client"] + }, + "include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts", "env.d.ts"] +}