add: 增加对CodeMirror的设置页

This commit is contained in:
rarnu
2026-05-02 14:58:55 +08:00
parent 0273f9f65d
commit 4af43e35e1
9 changed files with 465 additions and 23 deletions
@@ -0,0 +1,173 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { Settings } from "lucide-vue-next";
import { Button } from "@/components/ui/button";
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { useSettingsStore, EDITOR_THEMES, FONT_FAMILIES, type EditorTheme } from "@/stores/settingsStore";
const { t } = useI18n();
const settingsStore = useSettingsStore();
const props = defineProps<{
open: boolean;
}>();
const emit = defineEmits<{
"update:open": [value: boolean];
}>();
// Local edit state
const editFontFamily = ref(settingsStore.editorSettings.fontFamily);
const editFontSize = ref(settingsStore.editorSettings.fontSize);
const editTheme = ref(settingsStore.editorSettings.theme);
// Sync from store when dialog opens
watch(() => props.open, (open) => {
if (open) {
editFontFamily.value = settingsStore.editorSettings.fontFamily;
editFontSize.value = settingsStore.editorSettings.fontSize;
editTheme.value = settingsStore.editorSettings.theme;
}
});
function hasChanges(): boolean {
return (
editFontFamily.value !== settingsStore.editorSettings.fontFamily ||
editFontSize.value !== settingsStore.editorSettings.fontSize ||
editTheme.value !== settingsStore.editorSettings.theme
);
}
function applySettings() {
settingsStore.updateEditorSettings({
fontFamily: editFontFamily.value,
fontSize: editFontSize.value,
theme: editTheme.value,
});
emit("update:open", false);
}
function resetDefaults() {
editFontFamily.value = "'JetBrains Mono', 'Fira Code', monospace";
editFontSize.value = 13;
editTheme.value = "one-dark";
}
function onFontFamilyChange(v: any) {
if (typeof v === 'string') editFontFamily.value = v;
}
function onThemeChange(v: any) {
if (typeof v === 'string') editTheme.value = v as EditorTheme;
}
</script>
<template>
<Dialog :open="open" @update:open="(v: boolean) => emit('update:open', v)">
<DialogContent class="sm:max-w-[480px]">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<Settings class="h-4 w-4" />
{{ t('settings.title') }}
</DialogTitle>
</DialogHeader>
<div class="space-y-5 py-2">
<!-- Font Family -->
<div class="space-y-2">
<Label>{{ t('settings.fontFamily') }}</Label>
<Select :model-value="editFontFamily" @update:model-value="onFontFamilyChange">
<SelectTrigger>
<SelectValue :placeholder="t('settings.selectFont')" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="font in FONT_FAMILIES"
:key="font.value"
:value="font.value"
:style="{ fontFamily: font.value }"
>
{{ font.label }}
</SelectItem>
</SelectContent>
</Select>
<p class="text-xs text-muted-foreground leading-relaxed font-mono" :style="{ fontFamily: editFontFamily }">
SELECT * FROM users WHERE id = 1;
</p>
</div>
<Separator />
<!-- Font Size -->
<div class="space-y-2">
<div class="flex items-center justify-between">
<Label>{{ t('settings.fontSize') }}</Label>
<span class="text-xs text-muted-foreground tabular-nums">{{ editFontSize }}px</span>
</div>
<input
type="range"
min="10"
max="24"
step="1"
:value="editFontSize"
@input="editFontSize = Number(($event.target as HTMLInputElement).value)"
class="w-full accent-primary"
/>
<div class="flex items-center gap-2 text-xs text-muted-foreground">
<span>10px</span>
<span class="flex-1 border-b border-dashed border-muted-foreground/30" />
<span>24px</span>
</div>
</div>
<Separator />
<!-- Theme -->
<div class="space-y-2">
<Label>{{ t('settings.theme') }}</Label>
<Select :model-value="editTheme" @update:model-value="onThemeChange">
<SelectTrigger>
<SelectValue :placeholder="t('settings.selectTheme')" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="theme in EDITOR_THEMES"
:key="theme.value"
:value="theme.value"
>
<div class="flex items-center gap-2">
<span
class="h-3 w-3 rounded-full border"
:class="theme.dark ? 'bg-foreground border-foreground/20' : 'bg-muted-foreground/30 border-muted-foreground/40'"
/>
{{ theme.label }}
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter class="gap-2 sm:gap-0">
<Button variant="outline" @click="resetDefaults">
{{ t('settings.resetDefaults') }}
</Button>
<div class="flex-1" />
<Button variant="outline" @click="emit('update:open', false)">
{{ t('common.close') }}
</Button>
<Button :disabled="!hasChanges()" @click="applySettings">
{{ t('settings.apply') }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
+68 -20
View File
@@ -2,9 +2,11 @@
import { ref, onMounted, onBeforeUnmount, watch, shallowRef } from "vue";
import type { CompletionContext } from "@codemirror/autocomplete";
import type { EditorView as EditorViewType } from "@codemirror/view";
import type { Extension } from "@codemirror/state";
import { resolveExecutableSql } from "@/lib/sqlExecutionTarget";
import { formatSqlText, type SqlFormatDialect } from "@/lib/sqlFormatter";
import { useConnectionStore } from "@/stores/connectionStore";
import { useSettingsStore, type EditorTheme } from "@/stores/settingsStore";
import {
buildSqlCompletionItemsFromContext,
getSqlCompletionContext,
@@ -29,48 +31,72 @@ const emit = defineEmits<{
const editorRef = ref<HTMLDivElement>();
const view = shallowRef<EditorViewType | null>(null);
const connectionStore = useConnectionStore();
const DEFAULT_FONT_SIZE = 13;
const settingsStore = useSettingsStore();
const MIN_FONT_SIZE = 10;
const MAX_FONT_SIZE = 24;
let editorViewModule: typeof import("@codemirror/view") | null = null;
let fontSizeTheme: import("@codemirror/state").Compartment | null = null;
let fontFamilyTheme: import("@codemirror/state").Compartment | null = null;
let codeMirrorTheme: import("@codemirror/state").Compartment | null = null;
const savedFontSize = Number(localStorage.getItem("dbx-query-editor-font-size"));
const fontSize = ref(
Number.isFinite(savedFontSize)
? Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, savedFontSize))
: DEFAULT_FONT_SIZE,
);
async function loadTheme(theme: EditorTheme): Promise<Extension> {
switch (theme) {
case "one-dark":
return (await import("@codemirror/theme-one-dark")).oneDark;
case "vscode-dark":
return (await import("@uiw/codemirror-theme-vscode")).vscodeDark;
case "vscode-light":
return (await import("@uiw/codemirror-theme-vscode")).vscodeLight;
case "nord":
return (await import("@uiw/codemirror-theme-nord")).nord;
case "okaidia":
return (await import("@uiw/codemirror-theme-okaidia")).okaidia;
case "material":
return (await import("@uiw/codemirror-theme-material")).materialDark;
case "duotone-light":
return (await import("@uiw/codemirror-theme-duotone")).duotoneLight;
case "duotone-dark":
return (await import("@uiw/codemirror-theme-duotone")).duotoneDark;
case "xcode":
return (await import("@uiw/codemirror-theme-xcode")).xcodeLight;
default:
return (await import("@codemirror/theme-one-dark")).oneDark;
}
}
function fontTheme(EditorView: typeof import("@codemirror/view").EditorView, size: number) {
function fontTheme(EditorView: typeof import("@codemirror/view").EditorView, size: number, family: string) {
return EditorView.theme({
"&": { height: "100%", fontSize: `${size}px` },
".cm-scroller": { overflow: "auto" },
".cm-content": { fontFamily: "'JetBrains Mono', 'Fira Code', monospace" },
".cm-content": { fontFamily: family },
});
}
function setFontSize(size: number) {
const next = Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, size));
fontSize.value = next;
localStorage.setItem("dbx-query-editor-font-size", String(next));
if (view.value && fontSizeTheme && editorViewModule) {
const ss = settingsStore.editorSettings;
ss.fontSize = next;
settingsStore.updateEditorSettings({ fontSize: next });
if (view.value && fontSizeTheme && fontFamilyTheme && editorViewModule) {
view.value.dispatch({
effects: fontSizeTheme.reconfigure(fontTheme(editorViewModule.EditorView, next)),
effects: [
fontSizeTheme.reconfigure(fontTheme(editorViewModule.EditorView, next, ss.fontFamily)),
fontFamilyTheme.reconfigure(fontTheme(editorViewModule.EditorView, next, ss.fontFamily)),
],
});
}
}
function zoomIn() {
setFontSize(fontSize.value + 1);
setFontSize(settingsStore.editorSettings.fontSize + 1);
}
function zoomOut() {
setFontSize(fontSize.value - 1);
setFontSize(settingsStore.editorSettings.fontSize - 1);
}
function resetZoom() {
setFontSize(DEFAULT_FONT_SIZE);
setFontSize(13);
}
function selectedSqlFromView(currentView: EditorViewType): string {
@@ -162,18 +188,20 @@ onMounted(async () => {
{ EditorState, Compartment },
{ sql, MySQL, PostgreSQL, SQLDialect },
{ basicSetup },
{ oneDark },
{ autocompletion, startCompletion },
] = await Promise.all([
import("@codemirror/view"),
import("@codemirror/state"),
import("@codemirror/lang-sql"),
import("codemirror"),
import("@codemirror/theme-one-dark"),
import("@codemirror/autocomplete"),
]);
editorViewModule = { EditorView, keymap } as typeof import("@codemirror/view");
fontSizeTheme = new Compartment();
fontFamilyTheme = new Compartment();
codeMirrorTheme = new Compartment();
const ss = settingsStore.editorSettings;
const baseDialect = props.dialect === "postgres" ? PostgreSQL : MySQL;
const extraKeywords = "PIVOT UNPIVOT EXCLUDE REPLACE QUALIFY ASOF POSITIONAL ANTI SEMI SAMPLE TABLESAMPLE STRUCT MAP LIST ARRAY LAMBDA UNNEST LATERAL FILTER RECURSIVE SUMMARIZE PRAGMA READ_CSV READ_PARQUET READ_JSON DESCRIBE SHOW COPY EXPORT IMPORT";
@@ -220,6 +248,8 @@ onMounted(async () => {
},
]);
const theme = await loadTheme(ss.theme);
const state = EditorState.create({
doc: props.modelValue,
extensions: [
@@ -231,7 +261,7 @@ onMounted(async () => {
async (context: CompletionContext) => provideSqlCompletions(context.state, context.pos),
],
}),
oneDark,
codeMirrorTheme.of(theme),
runKeymap,
EditorView.updateListener.of((update) => {
if (update.docChanged) {
@@ -248,7 +278,8 @@ onMounted(async () => {
emit("selectionChange", selectedSqlFromView(update.view));
}
}),
fontSizeTheme.of(fontTheme(EditorView, fontSize.value)),
fontSizeTheme.of(fontTheme(EditorView, ss.fontSize, ss.fontFamily)),
fontFamilyTheme.of(fontTheme(EditorView, ss.fontSize, ss.fontFamily)),
EditorView.domEventHandlers({
wheel(event) {
if (!event.metaKey && !event.ctrlKey) return false;
@@ -282,6 +313,23 @@ watch(
}
);
// Reactively apply editor settings changes (theme, font family, font size)
watch(
() => settingsStore.editorSettings,
async (ss) => {
if (!view.value || !codeMirrorTheme || !fontFamilyTheme || !fontSizeTheme || !editorViewModule) return;
const themeExt = await loadTheme(ss.theme);
view.value.dispatch({
effects: [
codeMirrorTheme.reconfigure(themeExt),
fontFamilyTheme.reconfigure(fontTheme(editorViewModule.EditorView, ss.fontSize, ss.fontFamily)),
fontSizeTheme.reconfigure(fontTheme(editorViewModule.EditorView, ss.fontSize, ss.fontFamily)),
],
});
},
{ deep: true }
);
onBeforeUnmount(() => {
view.value?.destroy();
});