diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index c66e81129e..bf76ba2d4f 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -171,6 +171,7 @@ import { elasticsearchJsonResponseForResult } from "@/lib/elasticsearch/elastics import { elasticsearchProfileBodyForResult, parseElasticsearchProfile } from "@/lib/elasticsearch/elasticsearchProfile"; import * as api from "@/lib/backend/api"; import type { SqlInsertMode } from "@/lib/export/sqlInsertMode"; +import { queryResultExportBaseName } from "@/lib/export/saveTextFile"; import { applyMongoGridChangesToDocument, applyMongoGridChangesToDocumentBaseline, buildMongoUpdateDocument, formatMongoShellLiteral, serializeMongoDocumentId, type MongoInputValue } from "@/lib/mongo/mongoDocumentValues"; import type { DataGridSortMode } from "@/lib/dataGrid/dataGridSort"; import { isDataGridToolbarCompact, type DataGridReloadIntent } from "@/lib/dataGrid/dataGridToolbar"; @@ -421,6 +422,10 @@ function increaseTableFontSize() { setTableFontSize(tableFontSize.value + 1); } +/** Export file base name for the query result grid: the result's label (nearby + * comment or `schema.table`) names exports, matching the result tab (#9894). */ +const activeQueryResultExportBaseName = computed(() => queryResultExportBaseName(props.activeTab.result?.sourceLabel, props.activeTab.title)); + const activeTabDimension = computed(() => { const tab = props.activeTab; if (!tab.connectionId || tab.mode !== "vector") return undefined; @@ -2159,7 +2164,7 @@ defineExpose({ }) => queryStore.buildQueryResultExportRequest(activeTab.id, options) " :all-export-results="allResultExportSheets" - :export-file-base-name="activeTab.title" + :export-file-base-name="activeQueryResultExportBaseName" @update:order-by-input="(v: string) => (activeTab.orderByInput = v)" @local-column-filters-change="(filters: Record) => queryStore.updateDataGridLocalColumnFilters(activeTab.id, filters)" @reload="(sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number, intent?: DataGridReloadIntent) => emit('reload', activeTab.id, sql, searchText, whereInput, orderBy, limit, offset, intent)" diff --git a/apps/desktop/src/lib/__tests__/export/saveTextFile.spec.ts b/apps/desktop/src/lib/__tests__/export/saveTextFile.spec.ts new file mode 100644 index 0000000000..0ff015fffd --- /dev/null +++ b/apps/desktop/src/lib/__tests__/export/saveTextFile.spec.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { queryResultExportBaseName, sanitizeExportBaseName } from "@/lib/export/saveTextFile"; + +describe("queryResultExportBaseName", () => { + it("prefers the result label derived from the SQL's nearby comment", () => { + expect(queryResultExportBaseName("检查结果", "query 5")).toBe("检查结果"); + }); + + it("falls back to the tab title when the result carries no label", () => { + expect(queryResultExportBaseName(undefined, "query 5")).toBe("query 5"); + expect(queryResultExportBaseName("", "query 5")).toBe("query 5"); + expect(queryResultExportBaseName(" ", "query 5")).toBe("query 5"); + }); + + it("keeps the schema-qualified label so exported files stay identifiable", () => { + expect(queryResultExportBaseName("dbx.orders", "本地MySQL8.4@dbx")).toBe("dbx.orders"); + }); + + it("returns undefined when neither source provides a name", () => { + expect(queryResultExportBaseName(undefined, undefined)).toBeUndefined(); + expect(queryResultExportBaseName("", "")).toBeUndefined(); + }); + + it("keeps the sanitized base name free of path separators", () => { + expect(sanitizeExportBaseName(queryResultExportBaseName("检查结果/2026", "query 5")!)).toBe("检查结果_2026"); + }); +}); diff --git a/apps/desktop/src/lib/export/saveTextFile.ts b/apps/desktop/src/lib/export/saveTextFile.ts index a63c754524..de5dd2d74c 100644 --- a/apps/desktop/src/lib/export/saveTextFile.ts +++ b/apps/desktop/src/lib/export/saveTextFile.ts @@ -39,6 +39,19 @@ export async function saveTextFile(content: string, defaultFileName: string, fil return true; } +/** + * Export file base name for a query result (#9894). + * + * The result tab is already named after the SQL's nearby comment (`-- name: x` + * or a plain leading comment) or after the `schema.table` that produced it, so + * an export of that result should reuse the same name instead of the generic + * query tab title ("query 3", "查询 3"). Table data tabs keep passing their + * table name, which is why the caller decides the fallback. + */ +export function queryResultExportBaseName(resultLabel: string | undefined, tabTitle: string | undefined): string | undefined { + return resultLabel?.trim() || tabTitle?.trim() || undefined; +} + export function sanitizeExportBaseName(value: string): string { return replaceControlCharacters( value diff --git a/docs/screenshots/issue-9894-export-menu.png b/docs/screenshots/issue-9894-export-menu.png new file mode 100644 index 0000000000..2dcfc196ee Binary files /dev/null and b/docs/screenshots/issue-9894-export-menu.png differ diff --git a/docs/screenshots/issue-9894-result-tab-name.png b/docs/screenshots/issue-9894-result-tab-name.png new file mode 100644 index 0000000000..64b56760f9 Binary files /dev/null and b/docs/screenshots/issue-9894-result-tab-name.png differ