mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-31 16:24:33 +00:00
add: 增加对CodeMirror的设置页
This commit is contained in:
@@ -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>
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user