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": "打开失败: ",