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/.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/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" + } +} 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"] +} diff --git a/src/App.vue b/src/App.vue index 834ee47c..f78f1b5e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -367,6 +367,14 @@ :file-name="currentFileName" @close="showDiff = false"/> + + + (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('') @@ -1542,6 +1570,21 @@ const openDiff = () => { } 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 } @@ -2096,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() @@ -2171,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()}, @@ -2187,6 +2231,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()}, @@ -2200,18 +2245,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 +2271,6 @@ onMounted(async () => { // 恢复上次打开的文件标签 await restoreSession() - window.addEventListener('keydown', onGlobalKeydown, true) window.addEventListener('lsp:open-location', onLspOpenLocation) window.addEventListener('lsp:code-actions', onLspCodeActions) @@ -2246,7 +2280,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)) +} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 66383903..5f86652c 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -702,6 +702,8 @@ "toLowerCase": "Transform to lowercase", "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", @@ -1057,6 +1059,7 @@ }, "diff": { "title": "Diff", + "clipboardTitle": "Compare with clipboard", "close": "Close", "subtitle": "Saved (red) → Current (green)", "noDiff": "No differences, content is identical", @@ -1320,6 +1323,9 @@ "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", + "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 0b93894e..34ee5c19 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -702,6 +702,8 @@ "toLowerCase": "转为小写", "removeDuplicateLines": "删除重复行", "trimTrailingWhitespace": "去除行尾空白", + "copyAsMarkdown": "复制为 Markdown 代码块", + "compareClipboard": "与剪贴板比较", "groupText": "文本", "toggleAutoReveal": "切换:自动定位当前文件", "toggleSidebar": "切换侧栏", @@ -1057,6 +1059,7 @@ }, "diff": { "title": "差异对比", + "clipboardTitle": "与剪贴板比较", "close": "关闭", "subtitle": "已保存(红) → 当前(绿)", "noDiff": "没有差异,内容一致", @@ -1320,6 +1323,9 @@ "untitled": "未命名", "multiEngine": "该类型可用多个运行引擎,已选「{name}」,可在下拉手动切换", "pathCopied": "已复制路径", + "copiedMarkdown": "已复制为 Markdown 代码块", + "clipboardEmpty": "剪贴板为空", + "clipboardReadFailed": "读取剪贴板失败: ", "copyFailed": "复制失败: ", "notTextFile": "不是文本文件,无法打开", "openFailed": "打开失败: ",