feat(editor): add code snapshot export

This commit is contained in:
Abeautifulsnow authored and GitHub committed 2026-08-19 15:49:15 +08:00
1 parent a40dc02825
commit 30f5c34729
18 files changed
+774 -7

No files matched your search

@@ -9,6 +9,7 @@ import {
ArrowRightLeft,
AlertTriangle,
Bot,
Camera,
Check,
ChevronLeft,
ChevronRight,
@@ -44,6 +45,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useTheme } from "@/composables/useTheme";
import CodeSnapshotDialog from "@/components/codeSnapshot/CodeSnapshotDialog.vue";
import type { CodeSnapshotSource } from "@/lib/codeSnapshot/codeSnapshot";
import { useSettingsStore, AI_PROVIDER_PRESETS, normalizeAiConfig } from "@/stores/settingsStore";
import AiProviderLogo from "@/components/icons/AiProviderLogo.vue";
import { useConnectionStore } from "@/stores/connectionStore";
@@ -843,6 +846,14 @@ function sendProposalReply(positive: boolean) {
const activePlaceholder = computed(() => `${t(`ai.placeholders.${activeAction.value}`)} ${t("ai.tableMentionPlaceholderHint")}`);
const aiCodeAppearance = computed(() => (isDark.value ? "dark" : "light"));
const codeSnapshotOpen = ref(false);
const codeSnapshotSource = ref<CodeSnapshotSource | null>(null);
function openCodeSnapshot(seg: { content: string; lang: string }) {
codeSnapshotSource.value = { code: seg.content, lang: seg.lang };
codeSnapshotOpen.value = true;
}
const showActionButtons = computed(() => {
if (!props.connection) return true;
return !isVectorDbType(props.connection.db_type);
@@ -2640,6 +2651,9 @@ async function openExternalUrl(url: string) {
<Check v-if="copiedContentKey === `code:${i}:${j}`" class="h-3.5 w-3.5 text-green-400" />
<Copy v-else class="h-3.5 w-3.5" />
</button>
<button class="rounded p-0.5 text-zinc-500 hover:bg-zinc-200 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-700 dark:hover:text-zinc-200" :title="t('codeSnapshot.take')" @click="openCodeSnapshot(seg)">
<Camera class="h-3.5 w-3.5" />
</button>
</div>
</div>
<pre class="ai-code-block whitespace-pre-wrap break-words p-3 text-xs leading-relaxed text-zinc-900 dark:text-zinc-100"><code v-html="seg.html"></code></pre>
@@ -3116,6 +3130,7 @@ async function openExternalUrl(url: string) {
</div>
</div>
</div>
<CodeSnapshotDialog v-model:open="codeSnapshotOpen" :source="codeSnapshotSource" />
</div>
</div>
</template>
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, onActivated, onDeactivated, watch, shallowRef, computed, nextTick } from "vue";
import { AlignLeft, CaseLower, CaseUpper, ClipboardPaste, Code2, Download, FileCode, MessageSquareText, Minimize2, Pencil, PencilRuler, Play, Copy, List, Scissors, Search, Sparkles, Table2, TextSelect, Trash2 } from "@lucide/vue";
import { AlignLeft, Camera, CaseLower, CaseUpper, ClipboardPaste, Code2, Download, FileCode, MessageSquareText, Minimize2, Pencil, PencilRuler, Play, Copy, List, Scissors, Search, Sparkles, Table2, TextSelect, Trash2 } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import type { CompletionContext } from "@codemirror/autocomplete";
import { Transaction, StateEffect } from "@codemirror/state";
@@ -9,7 +9,9 @@ import { search as cmSearch } from "@codemirror/search";
import EditorSearchPanel from "./EditorSearchPanel.vue";
import SqlExecutionTargetPicker from "./SqlExecutionTargetPicker.vue";
import DelimitedListDialog from "./DelimitedListDialog.vue";
import CodeSnapshotDialog from "@/components/codeSnapshot/CodeSnapshotDialog.vue";
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
import type { CodeSnapshotSource } from "@/lib/codeSnapshot/codeSnapshot";
import { copyToClipboard, readTextFromClipboard } from "@/lib/common/clipboard";
import { completionMatchRanges } from "@/lib/common/completionMatch";
import { executionCandidateForMode, resolveExecutableSql, type SqlExecutionSnapshot, type SqlExecutionOverride, type SqlExecutionCandidate } from "@/lib/sql/sqlExecutionTarget";
@@ -368,6 +370,8 @@ const pickerAnchor = ref<{ left: number; top: number }>();
// Delimited list dialog state
const delimitedListOpen = ref(false);
const delimitedListSelectedText = ref("");
const codeSnapshotOpen = ref(false);
const codeSnapshotSource = ref<CodeSnapshotSource | null>(null);
function openDelimitedListDialog() {
if (props.readOnly) return;
@@ -1772,6 +1776,17 @@ const contextMenuItems = computed<ContextMenuItem[]>(() => {
icon: Copy,
shortcut: "Mod+C",
},
{
label: t("editor.contextMenu.screenshotSelection"),
action: () => {
if (selectedSql.value.trim()) {
codeSnapshotSource.value = { code: selectedSql.value, lang: "sql" };
codeSnapshotOpen.value = true;
}
},
disabled: !canCopySelectedSql.value,
icon: Camera,
},
{
label: t("editor.contextMenu.cutSelection"),
action: cutSelectedSqlFromContextMenu,
@@ -5843,6 +5858,7 @@ defineExpose({
<EditorSearchPanel ref="searchPanelRef" :view="view" />
<SqlExecutionTargetPicker v-if="pickerVisible" :candidates="pickerCandidates" :active-index="pickerActiveIndex" :anchor="pickerAnchor" @update:active-index="onPickerActiveIndexChange" @confirm="onPickerConfirm" @cancel="closePicker" />
<DelimitedListDialog v-model:open="delimitedListOpen" :selected-text="delimitedListSelectedText" @confirm="applyDelimitedListResult" />
<CodeSnapshotDialog v-model:open="codeSnapshotOpen" :source="codeSnapshotSource" />
<!-- SQL 意图操作弹出菜单(参考 DataGrip Alt+Enter) -->
<Teleport to="body">
<div v-if="intentionPopup?.visible" class="intention-popup-overlay" @click.self="closeIntentionPopup">