diff --git a/src/components/grid/DataGrid.vue b/src/components/grid/DataGrid.vue index 17072dc0cd..b720428854 100644 --- a/src/components/grid/DataGrid.vue +++ b/src/components/grid/DataGrid.vue @@ -32,6 +32,8 @@ import { RefreshCcw, RotateCcw, Pencil, + Filter, + FileDown, } from "lucide-vue-next"; import { Button } from "@/components/ui/button"; import { @@ -39,6 +41,9 @@ import { ContextMenuContent, ContextMenuItem, ContextMenuSeparator, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, ContextMenuTrigger, } from "@/components/ui/context-menu"; import { @@ -827,6 +832,14 @@ const selectedCellCount = computed(() => selectedCells.value.columns.length * se const hasCellSelection = computed(() => selectedCellCount.value > 0); const selectionSummary = computed(() => t("grid.selectedCells", { count: selectedCellCount.value })); const contextRowItem = computed(() => (contextCell.value ? getRowItem(contextCell.value.rowId) : undefined)); +const contextColumn = computed(() => { + if (!contextCell.value || contextCell.value.col < 0) return null; + return props.result.columns[contextCell.value.col] ?? null; +}); +const contextCellValue = computed(() => { + if (!contextCell.value || contextCell.value.col < 0) return null; + return contextRowItem.value?.data[contextCell.value.col] ?? null; +}); const activeCellDetail = computed(() => { const cell = detailCell.value; if (!cell) return null; @@ -954,6 +967,50 @@ function toggleSort(colName: string, colIdx: number) { } } +function applyContextSort(direction: "asc" | "desc" | null) { + if (!contextColumn.value || !contextCell.value) return; + const column = contextColumn.value; + const columnIndex = contextCell.value.col; + orderByInput.value = ""; + currentPage.value = 1; + if (direction) { + sortCol.value = column; + sortColIndex.value = columnIndex; + sortDir.value = direction; + } else { + sortCol.value = null; + sortColIndex.value = null; + sortDir.value = "asc"; + } + emit("sort", column, columnIndex, direction, currentWhereInput()); +} + +function contextFilterCondition(mode: "equals" | "not-equals" | "is-null" | "is-not-null"): string | null { + if (!contextColumn.value) return null; + const column = quoteIdent(contextColumn.value); + const value = contextCellValue.value; + + if (mode === "is-null") return `${column} IS NULL`; + if (mode === "is-not-null") return `${column} IS NOT NULL`; + if (value === null) return mode === "equals" ? `${column} IS NULL` : `${column} IS NOT NULL`; + return mode === "equals" ? `${column} = ${escapeVal(value)}` : `${column} <> ${escapeVal(value)}`; +} + +async function applyContextFilter(mode: "equals" | "not-equals" | "is-null" | "is-not-null") { + if (!canUseWhereSearch.value) return; + const condition = contextFilterCondition(mode); + if (!condition) return; + const existing = whereFilterInput.value.trim(); + whereFilterInput.value = existing ? `(${existing}) AND (${condition})` : condition; + await applyWhereFilter(); +} + +async function clearContextFilter() { + if (!canUseWhereSearch.value) return; + whereFilterInput.value = ""; + await applyWhereFilter(); +} + async function applyOrderBySearch() { if (!props.tableMeta || !props.onExecuteSql) return; const orderByClause = orderByInput.value.trim() || undefined; @@ -1326,6 +1383,12 @@ function selectSingleCell(rowIndex: number, colIndex: number) { selectionFocus.value = cell; } +function selectRow(rowIndex: number) { + if (props.result.columns.length === 0) return; + selectionAnchor.value = { rowIndex, colIndex: 0 }; + selectionFocus.value = { rowIndex, colIndex: props.result.columns.length - 1 }; +} + function finishCellSelection() { isSelectingCells.value = false; document.removeEventListener("mouseup", finishCellSelection); @@ -1445,8 +1508,13 @@ function onCellContext(rowId: number, rowIndex: number, colIdx: number) { } } +function onRowContext(rowId: number, rowIndex: number) { + contextCell.value = { rowId, rowIndex, col: -1 }; + selectRow(rowIndex); +} + function copyCell() { - if (!contextCell.value) return; + if (!contextCell.value || contextCell.value.col < 0) return; const item = getRowItem(contextCell.value.rowId); const val = item?.data[contextCell.value.col] ?? null; copyText(formatCell(val)); @@ -1983,9 +2051,11 @@ defineExpose({ :style="{ height: '26px', width: 'var(--total-w)' }" >
{{ index + 1 }}
@@ -2245,24 +2315,58 @@ defineExpose({ - - {{ t("grid.copyCell") }} - {{ t("grid.copyRow") }} - {{ t("grid.copyRowInsert") }} - {{ t("grid.copyAll") }} - + + + + {{ t("grid.copy") }} + + {{ t("grid.copyCell") }} + {{ t("grid.copyRow") }} + {{ t("grid.copyRowInsert") }} + {{ t("grid.copyAll") }} + + {{ t("grid.transpose") }} + + {{ t("grid.selection") }} + + {{ t("grid.copySelectionTsv") }} + {{ t("grid.copySelectionCsv") }} + {{ t("grid.copySelectionJson") }} + {{ t("grid.copySelectionSql") }} + + {{ t("grid.clearSelection") }} + + - - {{ t("grid.exportCsv") }} - {{ t("grid.exportJson") }} - {{ t("grid.exportMarkdown") }} + + {{ t("grid.export") }} + + {{ t("grid.exportCsv") }} + {{ t("grid.exportJson") }} + {{ t("grid.exportMarkdown") }} + + diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index bc56533525..76f489591e 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -172,18 +172,30 @@ export default { noSearchResultsDescription: "Try a different keyword.", noFilteredRows: "No rows match the filter", noFilteredRowsDescription: "Adjust the search text or row status filter.", + copy: "Copy", copyCell: "Copy Cell", copyRow: "Copy Row (JSON)", copyRowInsert: "Copy as INSERT", copyAll: "Copy All (TSV)", + selection: "Selection", copySelectionTsv: "Copy Selection (TSV)", copySelectionCsv: "Copy Selection (CSV)", copySelectionJson: "Copy Selection (JSON)", copySelectionSql: "Copy Selection as SQL IN List", clearSelection: "Clear Selection", + export: "Export", exportCsv: "Export CSV", exportJson: "Export JSON", exportMarkdown: "Export Markdown", + filter: "Filter", + filterByValue: "Filter by This Value", + filterExcludeValue: "Exclude This Value", + filterIsNull: "Show NULL Values", + filterIsNotNull: "Show Non-NULL Values", + clearFilter: "Clear Filter", + sortAscending: "Sort Ascending", + sortDescending: "Sort Descending", + clearSort: "Clear Sort", copied: "Copied!", search: "Search...", searchOrWhere: "Search, or enter a WHERE clause...", diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 1145d4dd22..8efed34af2 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -171,18 +171,30 @@ export default { noSearchResultsDescription: "换个关键词试试。", noFilteredRows: "没有符合条件的行", noFilteredRowsDescription: "调整搜索词或状态筛选试试。", + copy: "复制", copyCell: "复制单元格", copyRow: "复制行 (JSON)", copyRowInsert: "复制为 INSERT 语句", copyAll: "复制全部 (TSV)", + selection: "选区", copySelectionTsv: "复制选区 (TSV)", copySelectionCsv: "复制选区 (CSV)", copySelectionJson: "复制选区 (JSON)", copySelectionSql: "复制选区为 SQL IN 列表", clearSelection: "清除选区", + export: "导出", exportCsv: "导出 CSV", exportJson: "导出 JSON", exportMarkdown: "导出 Markdown", + filter: "筛选", + filterByValue: "筛选此值", + filterExcludeValue: "排除此值", + filterIsNull: "仅显示 NULL", + filterIsNotNull: "仅显示非 NULL", + clearFilter: "清除筛选", + sortAscending: "升序排序", + sortDescending: "降序排序", + clearSort: "清除排序", copied: "已复制!", search: "搜索...", searchOrWhere: "搜索,或输入 WHERE 条件...",