[Feat]:实现SQL变量替换 (#2597)

This commit is contained in:
miracle authored and GitHub committed 2026-07-06 13:21:45 +08:00
1 parent 987b0fd263
commit b118ca31ed
12 files changed
+306 -33

No files matched your search

@@ -9,7 +9,7 @@ import { Popover, PopoverAnchor, PopoverContent } from "@/components/ui/popover"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import TruncatedTextTooltip from "@/components/ui/TruncatedTextTooltip.vue";
import { loadSqlParameterHistory, rememberSqlParameterValues } from "@/lib/sql/sqlParameterHistory";
import { substituteSqlParameters, type SqlParameterInput, type SqlParameterValueKind } from "@/lib/sql/sqlParameters";
import { substituteSqlParameters, type SqlParameterDescriptor, type SqlParameterInput, type SqlParameterSyntax, type SqlParameterValueKind } from "@/lib/sql/sqlParameters";
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
const { t } = useI18n();
@@ -19,7 +19,7 @@ const open = defineModel<boolean>("open", { default: false });
const props = defineProps<{
sql: string;
parameters: string[];
parameters: SqlParameterDescriptor[];
}>();
const emit = defineEmits<{
@@ -33,6 +33,14 @@ let closeHistoryTimer: ReturnType<typeof setTimeout> | undefined;
const parameterKinds: SqlParameterValueKind[] = ["string", "number", "boolean", "null", "raw"];
const syntaxLabels: Record<SqlParameterSyntax, string> = {
positional: "?",
named: ":name",
shell: "${name}",
mybatis: "#{name}",
sqlserver: "@name",
};
const resolvedSql = computed(() => substituteSqlParameters(props.sql, values.value));
const highlightedSql = computed(() => highlight(resolvedSql.value));
@@ -42,10 +50,10 @@ watch(
if (!isOpen) return;
const next: Record<string, SqlParameterInput> = {};
const nextHistories: Record<string, SqlParameterInput[]> = {};
for (const name of props.parameters) {
const history = loadSqlParameterHistory(name);
nextHistories[name] = history;
next[name] = values.value[name] ?? history[0] ?? { kind: "string", value: "" };
for (const parameter of props.parameters) {
const history = loadSqlParameterHistory(parameter.key);
nextHistories[parameter.key] = history;
next[parameter.key] = values.value[parameter.key] ?? history[0] ?? { kind: "string", value: "" };
}
values.value = next;
histories.value = nextHistories;
@@ -111,15 +119,17 @@ function execute() {
<p class="text-sm text-muted-foreground">{{ t("sqlParameters.description") }}</p>
<div class="relative z-20 max-h-[302px] overflow-auto rounded-md border bg-background">
<div class="min-w-[580px]">
<div class="sticky top-0 z-10 grid grid-cols-[minmax(140px,1fr)_132px_minmax(180px,1.5fr)] border-b bg-muted px-3 py-2 text-xs font-medium text-muted-foreground">
<div class="min-w-[680px]">
<div class="sticky top-0 z-10 grid grid-cols-[minmax(140px,1fr)_104px_132px_minmax(180px,1.5fr)] border-b bg-muted px-3 py-2 text-xs font-medium text-muted-foreground">
<div>{{ t("sqlParameters.name") }}</div>
<div>{{ t("sqlParameters.syntax") }}</div>
<div>{{ t("sqlParameters.type") }}</div>
<div>{{ t("sqlParameters.value") }}</div>
</div>
<div v-for="name in parameters" :key="name" class="grid grid-cols-[minmax(140px,1fr)_132px_minmax(180px,1.5fr)] items-center gap-2 border-b px-3 py-2 text-sm last:border-b-0">
<div class="min-w-0 truncate font-mono text-xs">{{ name }}</div>
<Select :model-value="values[name]?.kind || 'string'" @update:model-value="(value) => updateKind(name, value as SqlParameterValueKind)">
<div v-for="parameter in parameters" :key="parameter.key" class="grid grid-cols-[minmax(140px,1fr)_104px_132px_minmax(180px,1.5fr)] items-center gap-2 border-b px-3 py-2 text-sm last:border-b-0">
<div class="min-w-0 truncate font-mono text-xs">{{ parameter.name }}</div>
<div class="min-w-0 truncate font-mono text-[11px] text-muted-foreground">{{ syntaxLabels[parameter.syntax] }}</div>
<Select :model-value="values[parameter.key]?.kind || 'string'" @update:model-value="(value) => updateKind(parameter.key, value as SqlParameterValueKind)">
<SelectTrigger class="h-8 bg-background text-xs">
<SelectValue />
</SelectTrigger>
@@ -130,28 +140,28 @@ function execute() {
</SelectContent>
</Select>
<div class="relative min-w-0">
<Popover :open="activeHistoryName === name && filteredSqlParameterHistory(name).length > 0">
<Popover :open="activeHistoryName === parameter.key && filteredSqlParameterHistory(parameter.key).length > 0">
<PopoverAnchor as-child>
<Input
:model-value="values[name]?.value || ''"
:model-value="values[parameter.key]?.value || ''"
class="h-8 bg-background font-mono text-xs"
:disabled="values[name]?.kind === 'null'"
:disabled="values[parameter.key]?.kind === 'null'"
autocomplete="off"
data-lpignore="true"
data-form-type="other"
:placeholder="t('sqlParameters.valuePlaceholder')"
@focus="focusParameterInput(name, $event)"
@blur="closeParameterHistory(name)"
@update:model-value="(value) => updateValue(name, String(value))"
@focus="focusParameterInput(parameter.key, $event)"
@blur="closeParameterHistory(parameter.key)"
@update:model-value="(value) => updateValue(parameter.key, String(value))"
/>
</PopoverAnchor>
<PopoverContent align="start" side="bottom" class="z-[80] w-[var(--reka-popover-trigger-width)] max-h-40 gap-0 overflow-auto p-1" @open-auto-focus.prevent>
<button
v-for="entry in filteredSqlParameterHistory(name)"
v-for="entry in filteredSqlParameterHistory(parameter.key)"
:key="`${entry.kind}:${entry.value}`"
type="button"
class="flex w-full min-w-0 items-center justify-between gap-2 rounded px-2 py-1 text-left text-xs hover:bg-accent hover:text-accent-foreground"
@mousedown.prevent="selectHistoryEntry(name, entry)"
@mousedown.prevent="selectHistoryEntry(parameter.key, entry)"
>
<TruncatedTextTooltip :text="entry.value" class="min-w-0 flex-1 font-mono" side="top" :delay="150" />
<span class="shrink-0 text-[10px] uppercase text-muted-foreground">{{ t(`sqlParameters.kind.${entry.kind}`) }}</span>