From 9261cb71a2ec3bfff875ec7dda5e52b229fdf81b Mon Sep 17 00:00:00 2001 From: rarnu Date: Sat, 9 May 2026 15:04:06 +0800 Subject: [PATCH] =?UTF-8?q?1.=20src/components/grid/DataGrid.vue=20=20=20?= =?UTF-8?q?=20=20=20=20=20-=20=E5=B7=A6=E4=BE=A7=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E6=A1=86=EF=BC=9A=E7=A7=BB=E9=99=A4=E4=BA=86=20WHERE=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=EF=BC=88isWhereSearch=20=E6=B0=B8=E8=BF=9C?= =?UTF-8?q?=E8=BF=94=E5=9B=9E=20false=EF=BC=89=EF=BC=8C=E7=8E=B0=E5=9C=A8?= =?UTF-8?q?=E6=98=AF=E7=BA=AF=E5=AE=A2=E6=88=B7=E7=AB=AF=E6=96=87=E6=9C=AC?= =?UTF-8?q?=E8=BF=87=E6=BB=A4=E3=80=82=E4=BF=9D=E7=95=99=E4=BA=86=E5=88=97?= =?UTF-8?q?=E5=90=8D=E4=BB=A3=E7=A0=81=E6=8F=90=E7=A4=BA=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=88Tab=20=E8=A1=A5=E5=85=A8=E3=80=81=E7=AE=AD=E5=A4=B4?= =?UTF-8?q?=E5=AF=BC=E8=88=AA=EF=BC=89=E3=80=82=20=20=20=20=20=20=20=20-?= =?UTF-8?q?=20WHERE=20=E6=A1=86=EF=BC=9A=E6=96=B0=E5=A2=9E=E5=88=97?= =?UTF-8?q?=E5=90=8D=E4=BB=A3=E7=A0=81=E6=8F=90=E7=A4=BA=EF=BC=88=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=E7=9A=84=E4=B8=8B=E6=8B=89=E5=BB=BA=E8=AE=AE=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=EF=BC=89=EF=BC=8C=E6=94=AF=E6=8C=81=E5=8D=95=E5=8F=8C?= =?UTF-8?q?=E5=BC=95=E5=8F=B7=E5=92=8C=E5=B0=8F=E6=8B=AC=E5=8F=B7=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E9=85=8D=E5=AF=B9=EF=BC=8CEnter=20=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E8=BF=87=E6=BB=A4=E3=80=82=20=20=20=20=20=20?= =?UTF-8?q?=20=20-=20ORDER=20BY=20=E6=A1=86=EF=BC=9A=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=88=97=E5=90=8D=E4=BB=A3=E7=A0=81=E6=8F=90=E7=A4=BA=EF=BC=88?= =?UTF-8?q?=E5=90=8C=E6=A0=B7=E7=9A=84=E5=BB=BA=E8=AE=AE=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=EF=BC=89=EF=BC=8CEnter=20=E7=9B=B4=E6=8E=A5=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E6=8E=92=E5=BA=8F=E3=80=82=20=20=20=20=20=20=20=20-=20?= =?UTF-8?q?=E5=88=B7=E6=96=B0/=E5=9B=9E=E6=BB=9A/=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=EF=BC=9Areload=20emit=20=E7=8E=B0=E5=9C=A8=E4=BC=A0=E9=80=92?= =?UTF-8?q?=204=20=E4=B8=AA=E5=8F=82=E6=95=B0=20(sql,=20searchText,=20wher?= =?UTF-8?q?eInput,=20orderBy)=EF=BC=8C=E7=A1=AE=E4=BF=9D=E5=88=B7=E6=96=B0?= =?UTF-8?q?=E5=90=8E=E6=89=80=E6=9C=89=E6=9D=A1=E4=BB=B6=E4=B8=8D=E4=B8=A2?= =?UTF-8?q?=E5=A4=B1=E3=80=82=20=20=20=20=20=20=20=20-=20=E5=88=86?= =?UTF-8?q?=E9=A1=B5/=E6=8E=92=E5=BA=8F=EF=BC=9A=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=96=B0=E7=9A=84=20currentWhereInput()=20?= =?UTF-8?q?=E5=87=BD=E6=95=B0=EF=BC=8C=E6=9B=BF=E4=BB=A3=E5=B7=B2=E5=BA=9F?= =?UTF-8?q?=E5=BC=83=E7=9A=84=20activeWhereInput=E3=80=82=20=20=20=20=20?= =?UTF-8?q?=20=20=20-=20=E5=88=A0=E9=99=A4=E4=BA=86=20applyWhereSearch()?= =?UTF-8?q?=20=E5=92=8C=20onSearchEnter()=EF=BC=88=E5=B7=A6=E4=BE=A7?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E4=B8=8D=E5=86=8D=E9=9C=80=E8=A6=81=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=20WHERE=20=E6=9F=A5=E8=AF=A2=EF=BC=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2. src/composables/useDataGridActions.ts - onReloadData() 签名改为接受 4 个参数 (sql, searchText, whereInput, orderBy)。 - table-data 模式刷新时,用 buildTableSql(tab, { whereInput, orderBy }) 重建 SQL,保留 WHERE 和 ORDER BY 条件。 3. src/components/layout/ContentArea.vue - reload emit 签名同步更新为 4 个参数。 - results 模式和 table-data 模式两个 DataGrid 的 @reload 处理器都转发全部 4 个参数。 4. src/App.vue - ContentArea 的 @reload 处理器转发 4 个参数给 onReloadData()。 --- src/App.vue | 5 +- src/components/grid/DataGrid.vue | 417 ++++++++++++++++++++------ src/components/layout/ContentArea.vue | 12 +- src/composables/useDataGridActions.ts | 4 +- 4 files changed, 345 insertions(+), 93 deletions(-) diff --git a/src/App.vue b/src/App.vue index 8c4fe193c2..4caef8ea4d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -509,7 +509,10 @@ onUnmounted(() => { @editor-selection-change="(v: string) => (selectedSql = v)" @editor-cursor-change="(p: number) => (cursorPos = p)" @format-error="toast(t('toolbar.formatSqlFailed'))" - @reload="(sql?: string, whereInput?: string) => onReloadData(sql, whereInput)" + @reload=" + (sql?: string, searchText?: string, whereInput?: string, orderBy?: string) => + onReloadData(sql, searchText, whereInput, orderBy) + " @paginate="onPaginate" @sort="onSort" @execute-sql="onExecuteSql" diff --git a/src/components/grid/DataGrid.vue b/src/components/grid/DataGrid.vue index ef7c6834b8..941e392022 100644 --- a/src/components/grid/DataGrid.vue +++ b/src/components/grid/DataGrid.vue @@ -64,7 +64,7 @@ import { type CellPosition, type CellSelectionRange, } from "@/lib/gridSelection"; -import { buildTableSelectSql, normalizeWhereInput, quoteTableIdentifier } from "@/lib/tableSelectSql"; +import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql"; import { buildDataGridSaveStatements, formatGridSqlLiteral } from "@/lib/dataGridSql"; import { formatMarkdownTable } from "@/lib/markdownTable"; import { @@ -98,7 +98,7 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - reload: [sql?: string, whereInput?: string]; + reload: [sql?: string, searchText?: string, whereInput?: string, orderBy?: string]; paginate: [offset: number, limit: number, whereInput?: string, orderBy?: string]; sort: [column: string, columnIndex: number, direction: "asc" | "desc" | null, whereInput?: string]; }>(); @@ -214,6 +214,23 @@ const searchInputRef = ref(); const measureRef = ref(); const suggestionLeft = ref(0); +const whereSuggestions = ref([]); +const whereSuggestionIndex = ref(-1); +const whereFilterInputRef = ref(); +const whereMeasureRef = ref(); +const whereSuggestionLeft = ref(0); + +const orderBySuggestions = ref([]); +const orderBySuggestionIndex = ref(-1); +const orderByInputRef = ref(); +const orderByMeasureRef = ref(); +const orderBySuggestionLeft = ref(0); + +const orderByInput = ref(""); +const hasOrderByInput = computed(() => orderByInput.value.trim().length > 0); +const whereFilterInput = ref(""); +const hasWhereFilterInput = computed(() => whereFilterInput.value.trim().length > 0); + function updateSuggestionPosition() { nextTick(() => { const input = searchInputRef.value; @@ -227,30 +244,20 @@ function updateSuggestionPosition() { watch(searchText, (val) => { searchSuggestions.value = []; - if (!canUseWhereSearch.value || !props.tableMeta?.columns?.length) return; + if (!props.tableMeta?.columns?.length) return; - const trimmed = val.trimStart(); - const lower = trimmed.toLowerCase(); + const trimmed = val.trim(); + if (trimmed.length === 0) return; - if (trimmed.length > 0 && lower !== "where" && "where".startsWith(lower)) { - searchSuggestions.value = ["WHERE "]; + const lastToken = trimmed.split(/[\s,()><=!&|]+/).pop() || ""; + if (lastToken.length > 0) { + const tl = lastToken.toLowerCase(); + searchSuggestions.value = props.tableMeta.columns + .map((c) => c.name) + .filter((n) => n.toLowerCase().startsWith(tl) && n.toLowerCase() !== tl) + .slice(0, 8); suggestionIndex.value = 0; updateSuggestionPosition(); - return; - } - - const m = val.match(/^\s*where\s+(.+)$/i); - if (m) { - const lastToken = m[1].split(/[\s,()><=!]+/).pop() || ""; - if (lastToken.length > 0) { - const tl = lastToken.toLowerCase(); - searchSuggestions.value = props.tableMeta.columns - .map((c) => c.name) - .filter((n) => n.toLowerCase().startsWith(tl) && n.toLowerCase() !== tl) - .slice(0, 8); - suggestionIndex.value = 0; - updateSuggestionPosition(); - } } }); @@ -259,17 +266,11 @@ function acceptSuggestion() { if (idx < 0 || idx >= searchSuggestions.value.length) return; const sug = searchSuggestions.value[idx]; - if (sug === "WHERE ") { - const trimmed = searchText.value.trimStart(); - const leading = searchText.value.slice(0, searchText.value.length - trimmed.length); - searchText.value = leading + "WHERE "; - } else { - const lastWordMatch = searchText.value.match(/([^\s,()><=!]+)$/); - if (lastWordMatch) { - const lastWord = lastWordMatch[1]; - const prefix = searchText.value.slice(0, -lastWord.length); - searchText.value = prefix + sug; - } + const lastWordMatch = searchText.value.match(/([^\s,()><=!&|]+)$/); + if (lastWordMatch) { + const lastWord = lastWordMatch[1]; + const prefix = searchText.value.slice(0, -lastWord.length); + searchText.value = prefix + sug; } searchSuggestions.value = []; suggestionIndex.value = -1; @@ -345,15 +346,212 @@ function onSearchKeydown(e: KeyboardEvent) { return; } } - if (e.key === "Enter") { - onSearchEnter(e); - return; - } if (e.key === "Escape") { searchText.value = ""; } } +// --- WHERE filter input suggestions --- +function updateWhereSuggestionPosition() { + nextTick(() => { + const input = whereFilterInputRef.value; + const measure = whereMeasureRef.value; + if (!input || !measure) return; + const cursorPos = input.selectionStart ?? 0; + measure.textContent = whereFilterInput.value.slice(0, cursorPos); + whereSuggestionLeft.value = measure.getBoundingClientRect().width; + }); +} + +function acceptWhereSuggestion() { + const idx = whereSuggestionIndex.value; + if (idx < 0 || idx >= whereSuggestions.value.length) return; + const sug = whereSuggestions.value[idx]; + const lastWordMatch = whereFilterInput.value.match(/([^\s,()><=!&|]+)$/); + if (lastWordMatch) { + const lastWord = lastWordMatch[1]; + const prefix = whereFilterInput.value.slice(0, -lastWord.length); + whereFilterInput.value = prefix + sug; + } + whereSuggestions.value = []; + whereSuggestionIndex.value = -1; + whereFilterInputRef.value?.focus(); +} + +function dismissWhereSuggestions() { + whereSuggestions.value = []; + whereSuggestionIndex.value = -1; +} + +function navigateWhereSuggestion(delta: number) { + if (whereSuggestions.value.length === 0) return; + whereSuggestionIndex.value = Math.min( + Math.max(whereSuggestionIndex.value + delta, 0), + whereSuggestions.value.length - 1, + ); +} + +watch(whereFilterInput, (val) => { + whereSuggestions.value = []; + if (!props.tableMeta?.columns?.length) return; + const trimmed = val.trim(); + if (trimmed.length === 0) return; + const lastToken = trimmed.split(/[\s,()><=!&|]+/).pop() || ""; + if (lastToken.length > 0) { + const tl = lastToken.toLowerCase(); + whereSuggestions.value = props.tableMeta.columns + .map((c) => c.name) + .filter((n) => n.toLowerCase().startsWith(tl) && n.toLowerCase() !== tl) + .slice(0, 8); + whereSuggestionIndex.value = 0; + updateWhereSuggestionPosition(); + } +}); + +function onWhereFilterKeydown(e: KeyboardEvent) { + if (e.key in PAIRS && !e.ctrlKey && !e.metaKey) { + const input = e.target as HTMLInputElement; + const start = input.selectionStart ?? 0; + const end = input.selectionEnd ?? 0; + const close = PAIRS[e.key]; + if (start !== end) { + e.preventDefault(); + const selected = whereFilterInput.value.slice(start, end); + whereFilterInput.value = + whereFilterInput.value.slice(0, start) + e.key + selected + close + whereFilterInput.value.slice(end); + nextTick(() => { + input.setSelectionRange(start + 1 + selected.length, start + 1 + selected.length); + }); + whereSuggestionIndex.value = -1; + return; + } + if (e.key === close && whereFilterInput.value[start] === close) { + e.preventDefault(); + input.setSelectionRange(start + 1, start + 1); + return; + } + e.preventDefault(); + whereFilterInput.value = whereFilterInput.value.slice(0, start) + e.key + close + whereFilterInput.value.slice(end); + nextTick(() => { + input.setSelectionRange(start + 1, start + 1); + }); + whereSuggestionIndex.value = -1; + return; + } + if (whereSuggestions.value.length > 0) { + if (e.key === "Tab") { + e.preventDefault(); + acceptWhereSuggestion(); + return; + } + if (e.key === "Escape") { + e.preventDefault(); + dismissWhereSuggestions(); + return; + } + if (e.key === "ArrowDown") { + e.preventDefault(); + navigateWhereSuggestion(1); + return; + } + if (e.key === "ArrowUp") { + e.preventDefault(); + navigateWhereSuggestion(-1); + return; + } + } + if (e.key === "Enter") { + e.preventDefault(); + applyWhereFilter(); + } +} + +// --- ORDER BY input suggestions --- +function updateOrderBySuggestionPosition() { + nextTick(() => { + const input = orderByInputRef.value; + const measure = orderByMeasureRef.value; + if (!input || !measure) return; + const cursorPos = input.selectionStart ?? 0; + measure.textContent = orderByInput.value.slice(0, cursorPos); + orderBySuggestionLeft.value = measure.getBoundingClientRect().width; + }); +} + +function acceptOrderBySuggestion() { + const idx = orderBySuggestionIndex.value; + if (idx < 0 || idx >= orderBySuggestions.value.length) return; + const sug = orderBySuggestions.value[idx]; + const lastWordMatch = orderByInput.value.match(/([^\s,()]+)$/); + if (lastWordMatch) { + const lastWord = lastWordMatch[1]; + const prefix = orderByInput.value.slice(0, -lastWord.length); + orderByInput.value = prefix + sug; + } + orderBySuggestions.value = []; + orderBySuggestionIndex.value = -1; + orderByInputRef.value?.focus(); +} + +function dismissOrderBySuggestions() { + orderBySuggestions.value = []; + orderBySuggestionIndex.value = -1; +} + +function navigateOrderBySuggestion(delta: number) { + if (orderBySuggestions.value.length === 0) return; + orderBySuggestionIndex.value = Math.min( + Math.max(orderBySuggestionIndex.value + delta, 0), + orderBySuggestions.value.length - 1, + ); +} + +watch(orderByInput, (val) => { + orderBySuggestions.value = []; + if (!props.tableMeta?.columns?.length) return; + const trimmed = val.trim(); + if (trimmed.length === 0) return; + const lastToken = trimmed.split(/[\s,()]+/).pop() || ""; + if (lastToken.length > 0 && !["asc", "desc", "asc", "desc"].includes(lastToken.toLowerCase())) { + const tl = lastToken.toLowerCase(); + orderBySuggestions.value = props.tableMeta.columns + .map((c) => c.name) + .filter((n) => n.toLowerCase().startsWith(tl) && n.toLowerCase() !== tl) + .slice(0, 8); + orderBySuggestionIndex.value = 0; + updateOrderBySuggestionPosition(); + } +}); + +function onOrderByKeydown(e: KeyboardEvent) { + if (orderBySuggestions.value.length > 0) { + if (e.key === "Tab") { + e.preventDefault(); + acceptOrderBySuggestion(); + return; + } + if (e.key === "Escape") { + e.preventDefault(); + dismissOrderBySuggestions(); + return; + } + if (e.key === "ArrowDown") { + e.preventDefault(); + navigateOrderBySuggestion(1); + return; + } + if (e.key === "ArrowUp") { + e.preventDefault(); + navigateOrderBySuggestion(-1); + return; + } + } + if (e.key === "Enter") { + e.preventDefault(); + applyOrderBySearch(); + } +} + const saveError = ref(""); const isApplyingWhere = ref(false); const rowStatusFilter = ref("all"); @@ -469,34 +667,33 @@ const currentPage = ref(1); const isFullPage = computed(() => props.result.rows.length >= pageSize.value); const isResultsContext = computed(() => props.context === "results"); const canUseWhereSearch = computed(() => !!props.tableMeta && !!props.onExecuteSql && !isResultsContext.value); -const isWhereSearch = computed(() => canUseWhereSearch.value && /^\s*where\b/i.test(searchText.value)); -const wherePredicate = computed(() => (canUseWhereSearch.value ? normalizeWhereInput(searchText.value) : undefined)); -const activeWhereInput = computed(() => (isWhereSearch.value && wherePredicate.value ? searchText.value : undefined)); -const clientSearchText = computed(() => (isWhereSearch.value ? "" : searchText.value)); +const clientSearchText = computed(() => (searchText.value.trim() ? searchText.value : "")); -const orderByInput = ref(""); -const hasOrderByInput = computed(() => orderByInput.value.trim().length > 0); -const whereFilterInput = ref(""); -const hasWhereFilterInput = computed(() => whereFilterInput.value.trim().length > 0); +function currentWhereInput(): string | undefined { + return whereFilterInput.value.trim() || undefined; +} function currentOrderBy(): string | undefined { - return sortCol.value ? `${quoteIdent(sortCol.value)} ${sortDir.value.toUpperCase()}` : undefined; + return ( + orderByInput.value.trim() || + (sortCol.value ? `${quoteIdent(sortCol.value)} ${sortDir.value.toUpperCase()}` : undefined) + ); } function prevPage() { if (currentPage.value <= 1) return; currentPage.value--; - emit("paginate", (currentPage.value - 1) * pageSize.value, pageSize.value, activeWhereInput.value, currentOrderBy()); + emit("paginate", (currentPage.value - 1) * pageSize.value, pageSize.value, currentWhereInput(), currentOrderBy()); } function nextPage() { if (!isFullPage.value) return; currentPage.value++; - emit("paginate", (currentPage.value - 1) * pageSize.value, pageSize.value, activeWhereInput.value, currentOrderBy()); + emit("paginate", (currentPage.value - 1) * pageSize.value, pageSize.value, currentWhereInput(), currentOrderBy()); } function changePageSize(size: number) { pageSize.value = size; currentPage.value = 1; - emit("paginate", 0, size, activeWhereInput.value, currentOrderBy()); + emit("paginate", 0, size, currentWhereInput(), currentOrderBy()); } // --- Editing --- @@ -532,7 +729,13 @@ async function onToolbarRefresh() { if (transactionActive.value) { discardChanges(); } - emit("reload", props.sql, searchText.value); + emit( + "reload", + props.sql, + searchText.value, + whereFilterInput.value.trim() || undefined, + orderByInput.value.trim() || undefined, + ); } async function onToolbarCommit() { @@ -541,6 +744,13 @@ async function onToolbarCommit() { function onToolbarRollback() { discardChanges(); + emit( + "reload", + props.sql, + searchText.value, + whereFilterInput.value.trim() || undefined, + orderByInput.value.trim() || undefined, + ); } const sortedRows = computed(() => { @@ -729,47 +939,18 @@ function toggleSort(colName: string, colIdx: number) { if (sortCol.value === colName && sortColIndex.value === colIdx) { if (sortDir.value === "asc") { sortDir.value = "desc"; - emit("sort", colName, colIdx, "desc", activeWhereInput.value); + emit("sort", colName, colIdx, "desc", currentWhereInput()); } else { sortCol.value = null; sortColIndex.value = null; sortDir.value = "asc"; - emit("sort", colName, colIdx, null, activeWhereInput.value); + emit("sort", colName, colIdx, null, currentWhereInput()); } } else { sortCol.value = colName; sortColIndex.value = colIdx; sortDir.value = "asc"; - emit("sort", colName, colIdx, "asc", activeWhereInput.value); - } -} - -function onSearchEnter(event: KeyboardEvent) { - if (!isWhereSearch.value) return; - event.preventDefault(); - void applyWhereSearch(); -} - -async function applyWhereSearch() { - if (!props.tableMeta || !props.onExecuteSql || !wherePredicate.value) return; - isApplyingWhere.value = true; - saveError.value = ""; - currentPage.value = 1; - try { - const sql = buildTableSelectSql({ - databaseType: props.databaseType, - schema: props.tableMeta.schema, - tableName: props.tableMeta.tableName, - primaryKeys: props.tableMeta.primaryKeys, - orderBy: sortCol.value ? `${quoteIdent(sortCol.value)} ${sortDir.value.toUpperCase()}` : undefined, - limit: pageSize.value, - whereInput: searchText.value, - }); - await props.onExecuteSql(sql); - } catch (e: any) { - saveError.value = String(e?.message || e); - } finally { - isApplyingWhere.value = false; + emit("sort", colName, colIdx, "asc", currentWhereInput()); } } @@ -1115,7 +1296,13 @@ async function saveChanges() { deletedRows.value.clear(); exitTransaction(); isSaving.value = false; - emit("reload", props.sql, searchText.value); + emit( + "reload", + props.sql, + searchText.value, + whereFilterInput.value.trim() || undefined, + orderByInput.value.trim() || undefined, + ); } function discardChanges() { @@ -1526,17 +1713,45 @@ defineExpose({