feat(grid): add row/column crosshair highlight setting
This commit is contained in:
1 parent
b09c6f4f21
commit
ec0b6768be
18 files changed
+496
-2
No files matched your search
@@ -415,6 +415,7 @@ const dataGridFilterViewPreviewExpanded = ref(true);
|
||||
const editDataGridTextFilterPanelHeight = ref(settingsStore.editorSettings.dataGridTextFilterPanelHeight);
|
||||
const editDataGridAutoTransposeSingleRow = ref(settingsStore.editorSettings.dataGridAutoTransposeSingleRow);
|
||||
const editDataGridCellDetailButtonVisible = ref(settingsStore.editorSettings.dataGridCellDetailButtonVisible);
|
||||
const editDataGridCrosshairHighlight = ref(settingsStore.editorSettings.dataGridCrosshairHighlight);
|
||||
const editPageSize = ref(settingsStore.editorSettings.pageSize);
|
||||
const editTableOpenPageSize = ref(settingsStore.editorSettings.tableOpenPageSize);
|
||||
const editQueryResultMaxRowsEnabled = ref(settingsStore.editorSettings.queryResultMaxRowsEnabled);
|
||||
@@ -607,6 +608,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft {
|
||||
dataGridTextFilterPanelHeight: editDataGridTextFilterPanelHeight.value,
|
||||
dataGridAutoTransposeSingleRow: editDataGridAutoTransposeSingleRow.value,
|
||||
dataGridCellDetailButtonVisible: editDataGridCellDetailButtonVisible.value,
|
||||
dataGridCrosshairHighlight: editDataGridCrosshairHighlight.value,
|
||||
flatteningMultiLineText: editFlatteningMultiLineText.value,
|
||||
pageSize: editPageSize.value,
|
||||
tableOpenPageSize: editTableOpenPageSize.value,
|
||||
@@ -871,6 +873,7 @@ function syncEditorSettingsDraftFromStore() {
|
||||
editDataGridTextFilterPanelHeight.value = settingsStore.editorSettings.dataGridTextFilterPanelHeight;
|
||||
editDataGridAutoTransposeSingleRow.value = settingsStore.editorSettings.dataGridAutoTransposeSingleRow;
|
||||
editDataGridCellDetailButtonVisible.value = settingsStore.editorSettings.dataGridCellDetailButtonVisible;
|
||||
editDataGridCrosshairHighlight.value = settingsStore.editorSettings.dataGridCrosshairHighlight;
|
||||
editFlatteningMultiLineText.value = settingsStore.editorSettings.flatteningMultiLineText;
|
||||
editPageSize.value = settingsStore.editorSettings.pageSize;
|
||||
editTableOpenPageSize.value = settingsStore.editorSettings.tableOpenPageSize;
|
||||
@@ -1170,6 +1173,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
|
||||
editDataGridTextFilterPanelHeight.value = DEFAULT_EDITOR_SETTINGS.dataGridTextFilterPanelHeight;
|
||||
editDataGridAutoTransposeSingleRow.value = DEFAULT_EDITOR_SETTINGS.dataGridAutoTransposeSingleRow;
|
||||
editDataGridCellDetailButtonVisible.value = DEFAULT_EDITOR_SETTINGS.dataGridCellDetailButtonVisible;
|
||||
editDataGridCrosshairHighlight.value = DEFAULT_EDITOR_SETTINGS.dataGridCrosshairHighlight;
|
||||
editFlatteningMultiLineText.value = DEFAULT_EDITOR_SETTINGS.flatteningMultiLineText;
|
||||
editPageSize.value = DEFAULT_EDITOR_SETTINGS.pageSize;
|
||||
editTableOpenPageSize.value = DEFAULT_EDITOR_SETTINGS.tableOpenPageSize;
|
||||
@@ -1252,6 +1256,7 @@ function resetAllDefaults() {
|
||||
editDataGridTextFilterPanelHeight.value = DEFAULT_EDITOR_SETTINGS.dataGridTextFilterPanelHeight;
|
||||
editDataGridAutoTransposeSingleRow.value = DEFAULT_EDITOR_SETTINGS.dataGridAutoTransposeSingleRow;
|
||||
editDataGridCellDetailButtonVisible.value = DEFAULT_EDITOR_SETTINGS.dataGridCellDetailButtonVisible;
|
||||
editDataGridCrosshairHighlight.value = DEFAULT_EDITOR_SETTINGS.dataGridCrosshairHighlight;
|
||||
editFlatteningMultiLineText.value = DEFAULT_EDITOR_SETTINGS.flatteningMultiLineText;
|
||||
editPageSize.value = DEFAULT_EDITOR_SETTINGS.pageSize;
|
||||
editTableOpenPageSize.value = DEFAULT_EDITOR_SETTINGS.tableOpenPageSize;
|
||||
@@ -5509,6 +5514,17 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<Switch id="data-grid-cell-detail-button-visible" v-model="editDataGridCellDetailButtonVisible" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="data-grid-crosshair-highlight">
|
||||
{{ t("settings.dataGridCrosshairHighlight") }}
|
||||
</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t("settings.dataGridCrosshairHighlightDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
<Switch id="data-grid-crosshair-highlight" v-model="editDataGridCrosshairHighlight" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="flattening-multi-line">
|
||||
|
||||
@@ -231,6 +231,7 @@ import {
|
||||
} from "@/lib/dataGrid/dataGridInfiniteScroll";
|
||||
import { resolveDataGridWheelScroll } from "@/lib/dataGrid/dataGridWheel";
|
||||
import { CANVAS_DATA_GRID_ROW_HEIGHT, MAX_CANVAS_DATA_GRID_PIXEL_RATIO, canvasDataGridActionOverlayWidth, canvasDataGridActionReservedWidth, dataGridSearchMatchKey, drawCanvasDataGrid, type CanvasDevicePixelSize } from "@/lib/dataGrid/canvasDataGridRenderer";
|
||||
import { resolveCrosshairTarget, type CrosshairTarget } from "@/lib/dataGrid/crosshairHighlight";
|
||||
import { DATA_GRID_DARK_STRIPED_ROW_BG, DATA_GRID_LIGHT_STRIPED_ROW_BG, dataGridActiveRowBackground } from "@/lib/dataGrid/dataGridPaintTheme";
|
||||
import { createRowLowerTextCache } from "@/lib/dataGrid/dataGridRowLowerText";
|
||||
import { dataGridPreviewLabelKey, dataGridSaveActionMode, dataGridSaveToolbarState } from "@/lib/dataGrid/dataGridSaveUi";
|
||||
@@ -387,6 +388,7 @@ const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const cellDetailButtonEnabled = computed(() => settingsStore.editorSettings.dataGridCellDetailButtonVisible);
|
||||
const dataGridCrosshairHighlight = computed(() => settingsStore.editorSettings.dataGridCrosshairHighlight);
|
||||
const tableFontSize = computed(() => settingsStore.editorSettings.tableFontSize);
|
||||
const rowNumberWidth = ref(DATA_GRID_ROW_NUM_WIDTH);
|
||||
const largeValueResolutionVersion = ref(0);
|
||||
@@ -5550,6 +5552,29 @@ const {
|
||||
isRowSelected,
|
||||
} = selection;
|
||||
|
||||
// 行列十字高亮目标:以 selectionFocus(原始坐标系)为中心解析,整行/整列选中时从
|
||||
// lastClickedRowIndex / selectedRowIds / selectedColumnIndexes 派生。开关关闭时恒为 null。
|
||||
const crosshairTarget = computed<CrosshairTarget | null>(() => {
|
||||
if (!dataGridCrosshairHighlight.value) return null;
|
||||
let fallbackRowIndex = selection.lastClickedRowIndex.value;
|
||||
if (fallbackRowIndex === null && selectedRowIds.value.size > 0) {
|
||||
const firstSelectedId = [...selectedRowIds.value][0];
|
||||
const firstSelectedIndex = displayItems.value.findIndex((item) => item.id === firstSelectedId);
|
||||
fallbackRowIndex = firstSelectedIndex >= 0 ? firstSelectedIndex : null;
|
||||
}
|
||||
if (fallbackRowIndex === null && transposeRowIndex.value !== null) fallbackRowIndex = transposeRowIndex.value;
|
||||
const fallbackColumnIndex = selectedColumnIndexes.value.size > 0 ? [...selectedColumnIndexes.value][0] : null;
|
||||
return resolveCrosshairTarget({
|
||||
selectionFocus: selectionFocus.value,
|
||||
selectionAnchor: selectionAnchor.value,
|
||||
hasRowSelection: hasRowSelection.value,
|
||||
hasColumnSelection: hasColumnSelection.value,
|
||||
visibleColumnIndexes: visibleColumnIndexes.value,
|
||||
fallbackRowIndex,
|
||||
fallbackColumnIndex,
|
||||
});
|
||||
});
|
||||
|
||||
function selectionCaptureBase(): Omit<CaptureDataGridSelectionOptions, "selectedRowIds" | "selectedColumnIndexes" | "selectedCellKeys" | "selectionAnchor" | "selectionFocus" | "selectingAll" | "lastClickedRowIndex"> {
|
||||
return {
|
||||
columns: props.result.columns,
|
||||
@@ -7674,6 +7699,7 @@ function drawCanvasGrid() {
|
||||
columnAligns: columnAligns.value,
|
||||
columnTypeVisualKinds: visibleColumnTypeVisualKinds.value,
|
||||
colorizeDataTypes: colorizeDataGridCellTypes.value,
|
||||
crosshair: crosshairTarget.value,
|
||||
rightAlignedActionCell: canvasRightAlignedActionCell.value,
|
||||
columnIsBoolean: isBooleanGridColumn,
|
||||
booleanDisplayMode: booleanDisplayMode.value,
|
||||
@@ -12295,6 +12321,7 @@ function openGridSnapshot() {
|
||||
'transpose-record-header-selected text-primary font-semibold': transposeRecordUsesFramedHeader(recordIndex),
|
||||
'transpose-record-header-active text-primary': transposeRecordUsesActiveHighlight(recordIndex) && !transposeRecordUsesFramedHeader(recordIndex),
|
||||
'data-grid-header-cell': !transposeRecordUsesActiveHighlight(recordIndex) && !transposeRecordUsesFramedHeader(recordIndex),
|
||||
'crosshair-column': !!crosshairTarget?.rowCrosshair && crosshairTarget.rowIndex === recordIndex && !transposeRecordUsesFramedHeader(recordIndex) && !transposeRecordUsesActiveHighlight(recordIndex),
|
||||
}"
|
||||
:style="{
|
||||
width: `${getTransposeRecordWidth(recordIndex)}px`,
|
||||
@@ -12374,6 +12401,8 @@ function openGridSnapshot() {
|
||||
'cell-selected-dirty--sparse': transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex],
|
||||
'row-cell-selected': transposeRecordUsesSelectionVisual(cell.recordIndex) && !transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && !displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex],
|
||||
'row-cell-selected-dirty': transposeRecordUsesSelectionVisual(cell.recordIndex) && !transposeCellIsSelected(cell.recordIndex, cell.valueIndex) && displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex],
|
||||
'crosshair-row':
|
||||
!!crosshairTarget?.columnCrosshair && cell.valueIndex === crosshairTarget.actualColIdx && !transposeRecordUsesSelectionVisual(cell.recordIndex) && !displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex] && !transposeCellIsSelected(cell.recordIndex, cell.valueIndex),
|
||||
'bg-primary/15': transposeRecordUsesActiveHighlight(cell.recordIndex) && !transposeRecordUsesSelectionVisual(cell.recordIndex) && !displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex] && !transposeCellIsSelected(cell.recordIndex, cell.valueIndex),
|
||||
'bg-yellow-500/10 cell-dirty': displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex],
|
||||
'bg-yellow-200/60 dark:bg-yellow-500/20': cellIsSearchMatch(cell.recordIndex, cell.valueIndex),
|
||||
@@ -12520,6 +12549,7 @@ function openGridSnapshot() {
|
||||
:name="col.name"
|
||||
:actual-column-index="col.actualColIdx"
|
||||
:visible-column-index="col.visibleColIdx"
|
||||
:class="{ 'crosshair-column': !!crosshairTarget?.columnCrosshair && crosshairTarget.visibleColIdx === col.visibleColIdx }"
|
||||
:selected="highlightedColumnIndex === col.actualColIdx || columnIsSelected(col.visibleColIdx)"
|
||||
:search-match="currentSearchMatch?.kind === 'column' && currentSearchMatch.col === col.actualColIdx"
|
||||
:dark="isDark"
|
||||
@@ -13275,6 +13305,7 @@ function openGridSnapshot() {
|
||||
'data-grid-row--draft': item.isDraft && !isRowActive(item.displayIndex),
|
||||
'data-grid-row--striped': !item.isNew && !item.isDraft && !item.isDeleted && !isRowActive(item.displayIndex) && item.displayIndex % 2 === 1,
|
||||
'active-row': isRowActive(item.displayIndex) && !item.isDeleted,
|
||||
'crosshair-row': !!crosshairTarget?.rowCrosshair && crosshairTarget.rowIndex === item.displayIndex && !item.isDeleted,
|
||||
'relative z-20 overflow-visible': editingCell?.rowId === item.id || readonlyTextCell?.rowId === item.id,
|
||||
}"
|
||||
:style="dataGridRowStyle(item)"
|
||||
@@ -13321,6 +13352,7 @@ function openGridSnapshot() {
|
||||
'cell-selected-dirty--sparse': selectionFramesData.sparse && cellIsSelected(item.displayIndex, col.visibleColIdx) && item.isDirtyCol[col.actualColIdx],
|
||||
'row-cell-selected': rowCellsUseSelectionVisual(item.id) && !cellIsSelected(item.displayIndex, col.visibleColIdx) && !item.isDirtyCol[col.actualColIdx],
|
||||
'row-cell-selected-dirty': rowCellsUseSelectionVisual(item.id) && !cellIsSelected(item.displayIndex, col.visibleColIdx) && item.isDirtyCol[col.actualColIdx],
|
||||
'crosshair-column': !!crosshairTarget?.columnCrosshair && crosshairTarget.visibleColIdx === col.visibleColIdx && !item.isDeleted,
|
||||
'cell-search-match': cellIsSearchMatch(item.displayIndex, col.actualColIdx),
|
||||
'cell-current-search-match': cellIsCurrentMatch(item.displayIndex, col.actualColIdx),
|
||||
'bg-yellow-200/60 dark:bg-yellow-500/20': cellIsSearchMatch(item.displayIndex, col.actualColIdx),
|
||||
@@ -14164,6 +14196,8 @@ function openGridSnapshot() {
|
||||
--data-grid-row-new-bg: rgb(243, 243, 243);
|
||||
--data-grid-row-deleted-bg: rgb(255, 244, 244);
|
||||
--data-grid-cell-active-bg: rgb(244, 248, 255);
|
||||
--data-grid-cell-crosshair-row-bg: rgb(174, 195, 224);
|
||||
--data-grid-cell-crosshair-col-bg: rgb(142, 170, 210);
|
||||
--data-grid-cell-dirty-bg: rgb(255, 248, 230);
|
||||
--data-grid-cell-selected-bg: rgb(239, 246, 255);
|
||||
--data-grid-cell-selected-single-bg: rgb(191, 219, 254);
|
||||
@@ -14196,6 +14230,8 @@ function openGridSnapshot() {
|
||||
--data-grid-row-new-bg: rgb(51, 51, 55);
|
||||
--data-grid-row-deleted-bg: rgb(55, 31, 32);
|
||||
--data-grid-cell-active-bg: rgb(25, 34, 46);
|
||||
--data-grid-cell-crosshair-row-bg: rgb(75, 84, 98);
|
||||
--data-grid-cell-crosshair-col-bg: rgb(98, 111, 130);
|
||||
--data-grid-cell-dirty-bg: rgb(94, 75, 26);
|
||||
--data-grid-cell-selected-bg: rgb(20, 40, 60);
|
||||
--data-grid-cell-selected-single-bg: rgb(30, 64, 96);
|
||||
@@ -14229,6 +14265,8 @@ function openGridSnapshot() {
|
||||
--data-grid-row-new-bg: color-mix(in oklab, var(--primary) 5%, transparent);
|
||||
--data-grid-row-deleted-bg: color-mix(in oklab, var(--destructive) 5%, transparent);
|
||||
--data-grid-cell-dirty-bg: color-mix(in oklab, rgb(240 177 0) 10%, transparent);
|
||||
--data-grid-cell-crosshair-row-bg: color-mix(in srgb, var(--primary) 34%, var(--background));
|
||||
--data-grid-cell-crosshair-col-bg: color-mix(in srgb, var(--primary) 50%, var(--background));
|
||||
--data-grid-cell-selected-bg: color-mix(in oklab, rgb(59 130 246) 12%, var(--background));
|
||||
--data-grid-cell-selected-single-bg: color-mix(in oklab, rgb(59 130 246) 30%, var(--background));
|
||||
--data-grid-cell-selected-dirty-bg: color-mix(in oklab, rgb(234 181 50) 30%, color-mix(in oklab, rgb(59 130 246) 18%, var(--background)));
|
||||
@@ -14239,6 +14277,11 @@ function openGridSnapshot() {
|
||||
--data-grid-row-number-deleted-bg: color-mix(in oklab, var(--destructive) 15%, var(--background));
|
||||
--data-grid-row-number-selected-bg: color-mix(in oklab, rgb(59 130 246) 30%, var(--background));
|
||||
}
|
||||
[data-grid-root].data-grid--dark,
|
||||
:global(.dark) [data-grid-root] {
|
||||
--data-grid-cell-crosshair-row-bg: color-mix(in srgb, var(--primary) 34%, var(--background));
|
||||
--data-grid-cell-crosshair-col-bg: color-mix(in srgb, var(--primary) 50%, var(--background));
|
||||
}
|
||||
[data-grid-root].data-grid--has-save-error {
|
||||
--data-grid-cell-dirty-bg: rgb(250, 212, 216) !important;
|
||||
--data-grid-cell-selected-dirty-bg: rgb(240, 192, 198) !important;
|
||||
@@ -14301,6 +14344,18 @@ function openGridSnapshot() {
|
||||
background-color: var(--data-grid-cell-bg);
|
||||
}
|
||||
|
||||
/* 行列十字高亮:覆盖基础单元格变量而非只涂父行,避免不透明 data-grid-cell /
|
||||
* frozen cell 遮住行高亮。选中、脏数据和搜索状态仍用各自的显式背景覆盖它。 */
|
||||
.crosshair-row {
|
||||
--data-grid-cell-bg: var(--data-grid-cell-crosshair-row-bg) !important;
|
||||
background-color: var(--data-grid-cell-crosshair-row-bg);
|
||||
}
|
||||
|
||||
.crosshair-column {
|
||||
--data-grid-cell-bg: var(--data-grid-cell-crosshair-col-bg) !important;
|
||||
background-color: var(--data-grid-cell-crosshair-col-bg);
|
||||
}
|
||||
|
||||
.data-grid-cell {
|
||||
background-color: var(--data-grid-cell-bg);
|
||||
}
|
||||
|
||||
@@ -6341,6 +6341,8 @@ export default {
|
||||
dataGridAutoTransposeSingleRowDescription: "When enabled, SQL query results with exactly one row and multiple columns automatically switch to transpose view.",
|
||||
dataGridCellDetailButtonVisible: "Show cell detail button",
|
||||
dataGridCellDetailButtonVisibleDescription: "Show the cell detail button when hovering over a cell.",
|
||||
dataGridCrosshairHighlight: "Crosshair row & column highlight",
|
||||
dataGridCrosshairHighlightDescription: "When enabled, lightly highlight the entire row and column of the active cell (like Excel). The focused cell keeps its selected style.",
|
||||
infiniteScroll: "Infinite scroll loading",
|
||||
autoCalculateTotalRows: "Auto-calculate total row count",
|
||||
autoCalculateTotalRowsDescription: "Run COUNT(*) automatically after each query to show the total matching rows. Off by default to keep large queries fast — calculate it on demand from the result footer.",
|
||||
|
||||
@@ -6043,6 +6043,8 @@ export default withEnglishFallback({
|
||||
dataGridAutoTransposeSingleRowDescription: "Al activarlo, los resultados de consultas SQL con exactamente una fila y varias columnas cambian automáticamente a la vista transpuesta.",
|
||||
dataGridCellDetailButtonVisible: "Mostrar botón de detalles de celda",
|
||||
dataGridCellDetailButtonVisibleDescription: "Mostrar el botón de detalles de celda al pasar el cursor sobre una celda.",
|
||||
dataGridCrosshairHighlight: "Resaltado de fila y columna en cruz",
|
||||
dataGridCrosshairHighlightDescription: "Cuando esté activado, resalta ligeramente toda la fila y la columna de la celda activa (como Excel). La celda enfocada conserva su estilo seleccionado.",
|
||||
infiniteScroll: "Carga de desplazamiento infinito",
|
||||
autoCalculateTotalRows: "Calcular automáticamente el total de filas",
|
||||
autoCalculateTotalRowsDescription: "Ejecuta COUNT(*) automáticamente tras cada consulta para mostrar el total de filas coincidentes. Desactivado por defecto para mantener rápidas las consultas grandes; puedes calcularlo cuando quieras desde el pie de resultados.",
|
||||
|
||||
@@ -6043,6 +6043,8 @@ export default withEnglishFallback({
|
||||
dataGridAutoTransposeSingleRowDescription: "Quando è attivata, i risultati delle query SQL con esattamente una riga e più colonne passano automaticamente alla vista trasposta.",
|
||||
dataGridCellDetailButtonVisible: "Mostra il pulsante dei dettagli della cella",
|
||||
dataGridCellDetailButtonVisibleDescription: "Mostra il pulsante dei dettagli della cella al passaggio del mouse su una cella.",
|
||||
dataGridCrosshairHighlight: "Evidenzia riga e colonna a croce",
|
||||
dataGridCrosshairHighlightDescription: "Se attivato, evidenzia leggermente l'intera riga e la colonna della cella attiva (come Excel). La cella focalizzata mantiene il suo stile selezionato.",
|
||||
infiniteScroll: "Caricamento a scorrimento infinito",
|
||||
autoCalculateTotalRows: "Calcola automaticamente il totale delle righe",
|
||||
autoCalculateTotalRowsDescription: "Esegue COUNT(*) automaticamente dopo ogni query per mostrare il totale delle righe corrispondenti. Disattivato per impostazione predefinita per mantenere veloci le query grandi; puoi calcolarlo all'occorrenza dal piè di pagina dei risultati.",
|
||||
|
||||
@@ -6077,6 +6077,8 @@ export default withEnglishFallback({
|
||||
dataGridAutoTransposeSingleRowDescription: "有効にすると、1 行かつ複数列の SQL クエリ結果が自動的に転置表示に切り替わります。",
|
||||
dataGridCellDetailButtonVisible: "セル詳細ボタンを表示",
|
||||
dataGridCellDetailButtonVisibleDescription: "セルにカーソルを合わせたときにセル詳細ボタンを表示します。",
|
||||
dataGridCrosshairHighlight: "行と列の十字ハイライト",
|
||||
dataGridCrosshairHighlightDescription: "有効にすると、アクティブなセルの行と列全体を淡くハイライトします(Excel と同様)。フォーカスされたセルは選択スタイルを維持します。",
|
||||
tableColumnTemplateFields: "新規テーブルのプリセット列",
|
||||
tableColumnTemplateFieldsDescription: "データベース種別を選択し、新規テーブル作成時に使うプリセット列の型を設定します。",
|
||||
tableColumnTemplateAdd: "列を追加",
|
||||
|
||||
@@ -5738,6 +5738,8 @@ export default withEnglishFallback({
|
||||
dataGridAutoTransposeSingleRowDescription: "활성화하면 정확히 한 행과 여러 열이 있는 SQL 쿼리 결과가 자동으로 전치 보기로 전환됩니다.",
|
||||
dataGridCellDetailButtonVisible: "셀 세부 정보 버튼 표시",
|
||||
dataGridCellDetailButtonVisibleDescription: "셀 위에 마우스를 올리면 셀 세부 정보 버튼을 표시합니다.",
|
||||
dataGridCrosshairHighlight: "행·열 십자 강조",
|
||||
dataGridCrosshairHighlightDescription: "활성화하면 활성 셀의 전체 행과 열을 연하게 강조합니다(Excel과 유사). 포커스 셀은 기존 선택 스타일을 유지합니다.",
|
||||
infiniteScroll: "무한 스크롤 로딩",
|
||||
autoCalculateTotalRows: "전체 행 수 자동 집계",
|
||||
autoCalculateTotalRowsDescription: "각 쿼리 후 자동으로 COUNT(*)를 실행하여 일치하는 전체 행 수를 표시합니다. 대규모 쿼리를 빠르게 유지하기 위해 기본적으로 꺼져 있으며, 결과 푸터에서 요청 시 집계할 수 있습니다.",
|
||||
|
||||
@@ -6045,6 +6045,8 @@ export default withEnglishFallback({
|
||||
dataGridAutoTransposeSingleRowDescription: "Quando ativado, os resultados de consultas SQL com exatamente uma linha e várias colunas alternam automaticamente para a visualização transposta.",
|
||||
dataGridCellDetailButtonVisible: "Mostrar botão de detalhes da célula",
|
||||
dataGridCellDetailButtonVisibleDescription: "Mostrar o botão de detalhes da célula ao passar o mouse sobre uma célula.",
|
||||
dataGridCrosshairHighlight: "Destacar linha e coluna em cruz",
|
||||
dataGridCrosshairHighlightDescription: "Quando ativado, destaca levemente toda a linha e a coluna da célula ativa (como no Excel). A célula focada mantém o estilo selecionado.",
|
||||
infiniteScroll: "Carregamento por rolagem infinita",
|
||||
autoCalculateTotalRows: "Calcular automaticamente o total de linhas",
|
||||
autoCalculateTotalRowsDescription: "Executa COUNT(*) automaticamente após cada consulta para mostrar o total de linhas correspondentes. Desativado por padrão para manter consultas grandes rápidas; você pode calculá-lo quando quiser no rodapé dos resultados.",
|
||||
|
||||
@@ -6325,6 +6325,8 @@ export default withEnglishFallback({
|
||||
dataGridAutoTransposeSingleRowDescription: "开启后,SQL 查询结果只有一行且包含多列时,自动切换为转置视图。",
|
||||
dataGridCellDetailButtonVisible: "显示单元格详情按钮",
|
||||
dataGridCellDetailButtonVisibleDescription: "鼠标悬停在单元格上时显示单元格详情按钮。",
|
||||
dataGridCrosshairHighlight: "行列十字高亮",
|
||||
dataGridCrosshairHighlightDescription: "开启后,以当前活动单元格为中心淡色高亮整行和整列(类似 Excel)。焦点单元格仍保留原有的选中样式。",
|
||||
infiniteScroll: "无限滚动加载",
|
||||
autoCalculateTotalRows: "自动统计总行数",
|
||||
autoCalculateTotalRowsDescription: "每次查询后自动执行 COUNT(*) 显示匹配的总行数。默认关闭以保证大查询速度 —— 可在结果栏按需手动统计。",
|
||||
|
||||
@@ -5362,6 +5362,8 @@ export default withEnglishFallback({
|
||||
dataGridAutoTransposeSingleRowDescription: "開啟後,SQL 查詢結果只有一行且包含多個欄位時,會自動切換為轉置檢視。",
|
||||
dataGridCellDetailButtonVisible: "顯示儲存格詳細資料按鈕",
|
||||
dataGridCellDetailButtonVisibleDescription: "游標懸停在儲存格上時顯示儲存格詳細資料按鈕。",
|
||||
dataGridCrosshairHighlight: "行列十字高亮",
|
||||
dataGridCrosshairHighlightDescription: "開啟後,以目前活動儲存格為中心淡色高亮整列和整行(類似 Excel)。焦點儲存格仍保留原有選取樣式。",
|
||||
infiniteScroll: "無限滾動載入",
|
||||
autoCalculateTotalRows: "自動統計總筆數",
|
||||
autoCalculateTotalRowsDescription: "每次查詢後自動執行 COUNT(*) 顯示符合的總筆數。預設關閉以確保大型查詢速度 —— 可在結果列按需手動統計。",
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveCrosshairTarget } from "@/lib/dataGrid/crosshairHighlight";
|
||||
import type { CellPosition } from "@/lib/dataGrid/gridSelection";
|
||||
|
||||
function focus(rowIndex: number, colIndex: number): CellPosition {
|
||||
return { rowIndex, colIndex };
|
||||
}
|
||||
|
||||
describe("resolveCrosshairTarget", () => {
|
||||
const visibleColumnIndexes = [3, 7, 11];
|
||||
|
||||
it("resolves both axes from selectionFocus for a plain cell", () => {
|
||||
const target = resolveCrosshairTarget({
|
||||
selectionFocus: focus(2, 1),
|
||||
selectionAnchor: focus(2, 1),
|
||||
hasRowSelection: false,
|
||||
hasColumnSelection: false,
|
||||
visibleColumnIndexes,
|
||||
});
|
||||
expect(target).toEqual({
|
||||
rowIndex: 2,
|
||||
visibleColIdx: 1,
|
||||
actualColIdx: 7,
|
||||
rowCrosshair: true,
|
||||
columnCrosshair: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps both axes for a multi-cell range (anchor + focus)", () => {
|
||||
const target = resolveCrosshairTarget({
|
||||
selectionFocus: focus(5, 2),
|
||||
selectionAnchor: focus(1, 0),
|
||||
hasRowSelection: false,
|
||||
hasColumnSelection: false,
|
||||
visibleColumnIndexes,
|
||||
});
|
||||
expect(target).toEqual({
|
||||
rowIndex: 5,
|
||||
visibleColIdx: 2,
|
||||
actualColIdx: 11,
|
||||
rowCrosshair: true,
|
||||
columnCrosshair: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("only highlights the row when hasRowSelection is set alongside focus", () => {
|
||||
const target = resolveCrosshairTarget({
|
||||
selectionFocus: focus(4, 0),
|
||||
selectionAnchor: focus(4, 0),
|
||||
hasRowSelection: true,
|
||||
hasColumnSelection: false,
|
||||
visibleColumnIndexes,
|
||||
});
|
||||
expect(target?.rowCrosshair).toBe(true);
|
||||
expect(target?.columnCrosshair).toBe(false);
|
||||
expect(target?.rowIndex).toBe(4);
|
||||
});
|
||||
|
||||
it("only highlights the column when hasColumnSelection is set alongside focus", () => {
|
||||
const target = resolveCrosshairTarget({
|
||||
selectionFocus: focus(4, 0),
|
||||
selectionAnchor: focus(4, 0),
|
||||
hasRowSelection: false,
|
||||
hasColumnSelection: true,
|
||||
visibleColumnIndexes,
|
||||
});
|
||||
expect(target?.rowCrosshair).toBe(false);
|
||||
expect(target?.columnCrosshair).toBe(true);
|
||||
expect(target?.visibleColIdx).toBe(0);
|
||||
});
|
||||
|
||||
it("derives the row from fallbackRowIndex for a whole-row selection with no focus", () => {
|
||||
const target = resolveCrosshairTarget({
|
||||
selectionFocus: null,
|
||||
selectionAnchor: null,
|
||||
hasRowSelection: true,
|
||||
hasColumnSelection: false,
|
||||
visibleColumnIndexes,
|
||||
fallbackRowIndex: 9,
|
||||
});
|
||||
expect(target).toEqual({
|
||||
rowIndex: 9,
|
||||
visibleColIdx: 0,
|
||||
actualColIdx: 3,
|
||||
rowCrosshair: true,
|
||||
columnCrosshair: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null for a whole-row selection when fallbackRowIndex is missing", () => {
|
||||
const target = resolveCrosshairTarget({
|
||||
selectionFocus: null,
|
||||
selectionAnchor: null,
|
||||
hasRowSelection: true,
|
||||
hasColumnSelection: false,
|
||||
visibleColumnIndexes,
|
||||
});
|
||||
expect(target).toBeNull();
|
||||
});
|
||||
|
||||
it("derives the column from fallbackColumnIndex for a whole-column selection with no focus", () => {
|
||||
const target = resolveCrosshairTarget({
|
||||
selectionFocus: null,
|
||||
selectionAnchor: null,
|
||||
hasRowSelection: false,
|
||||
hasColumnSelection: true,
|
||||
visibleColumnIndexes,
|
||||
fallbackColumnIndex: 2,
|
||||
});
|
||||
expect(target).toEqual({
|
||||
rowIndex: 0,
|
||||
visibleColIdx: 2,
|
||||
actualColIdx: 11,
|
||||
rowCrosshair: false,
|
||||
columnCrosshair: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null for a whole-column selection when fallbackColumnIndex is missing", () => {
|
||||
const target = resolveCrosshairTarget({
|
||||
selectionFocus: null,
|
||||
selectionAnchor: null,
|
||||
hasRowSelection: false,
|
||||
hasColumnSelection: true,
|
||||
visibleColumnIndexes,
|
||||
});
|
||||
expect(target).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when there is no focus and no row/column selection", () => {
|
||||
const target = resolveCrosshairTarget({
|
||||
selectionFocus: null,
|
||||
selectionAnchor: null,
|
||||
hasRowSelection: false,
|
||||
hasColumnSelection: false,
|
||||
visibleColumnIndexes,
|
||||
});
|
||||
expect(target).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when selectionFocus.colIndex is out of range for visibleColumnIndexes", () => {
|
||||
const target = resolveCrosshairTarget({
|
||||
selectionFocus: focus(0, 5),
|
||||
selectionAnchor: focus(0, 5),
|
||||
hasRowSelection: false,
|
||||
hasColumnSelection: false,
|
||||
visibleColumnIndexes,
|
||||
});
|
||||
expect(target).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when fallbackColumnIndex is out of range for visibleColumnIndexes", () => {
|
||||
const target = resolveCrosshairTarget({
|
||||
selectionFocus: null,
|
||||
selectionAnchor: null,
|
||||
hasRowSelection: false,
|
||||
hasColumnSelection: true,
|
||||
visibleColumnIndexes,
|
||||
fallbackColumnIndex: 9,
|
||||
});
|
||||
expect(target).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when visibleColumnIndexes is empty even with a focus", () => {
|
||||
const target = resolveCrosshairTarget({
|
||||
selectionFocus: focus(0, 0),
|
||||
selectionAnchor: focus(0, 0),
|
||||
hasRowSelection: false,
|
||||
hasColumnSelection: false,
|
||||
visibleColumnIndexes: [],
|
||||
});
|
||||
expect(target).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,15 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { DATA_GRID_DARK_ACTIVE_ROW_BG, DATA_GRID_LIGHT_ACTIVE_ROW_BG, dataGridActiveRowBackground, resolveDataGridPaintTheme } from "@/lib/dataGrid/dataGridPaintTheme";
|
||||
import {
|
||||
DATA_GRID_DARK_ACTIVE_ROW_BG,
|
||||
DATA_GRID_DARK_CROSSHAIR_COL_BG,
|
||||
DATA_GRID_DARK_CROSSHAIR_ROW_BG,
|
||||
DATA_GRID_LIGHT_ACTIVE_ROW_BG,
|
||||
DATA_GRID_LIGHT_CROSSHAIR_COL_BG,
|
||||
DATA_GRID_LIGHT_CROSSHAIR_ROW_BG,
|
||||
dataGridActiveRowBackground,
|
||||
resolveDataGridPaintTheme,
|
||||
} from "@/lib/dataGrid/dataGridPaintTheme";
|
||||
import { DEFAULT_DATA_GRID_TYPE_COLORS_DARK, DEFAULT_DATA_GRID_TYPE_COLORS_LIGHT, dataGridTypeColorCssVar } from "@/lib/dataGrid/dataGridTypeColorScheme";
|
||||
|
||||
function parseRgb(value: string): { r: number; g: number; b: number } | null {
|
||||
@@ -42,6 +51,69 @@ describe("data grid paint theme", () => {
|
||||
expect(resolveDataGridPaintTheme({ getVar: emptyCssVariable, isDark: true }).rowNumberActive).toBe(DATA_GRID_DARK_ACTIVE_ROW_BG);
|
||||
});
|
||||
|
||||
it("resolves crosshair row/col fills in both color schemes", () => {
|
||||
const emptyCssVariable = () => "";
|
||||
|
||||
const light = resolveDataGridPaintTheme({ getVar: emptyCssVariable, isDark: false });
|
||||
const dark = resolveDataGridPaintTheme({ getVar: emptyCssVariable, isDark: true });
|
||||
|
||||
// 十字底色必须比 cellActive 更明显(与 cellActive 不同、列比行更深的蓝色系)
|
||||
expect(light.cellCrosshairRow).toBe(DATA_GRID_LIGHT_CROSSHAIR_ROW_BG);
|
||||
expect(light.cellCrosshairCol).toBe(DATA_GRID_LIGHT_CROSSHAIR_COL_BG);
|
||||
expect(light.cellCrosshairRow).not.toBe(light.cellActive);
|
||||
expect(light.cellCrosshairCol).not.toBe(light.cellActive);
|
||||
expect(light.cellCrosshairCol).not.toBe(light.cellCrosshairRow);
|
||||
|
||||
expect(dark.cellCrosshairRow).toBe(DATA_GRID_DARK_CROSSHAIR_ROW_BG);
|
||||
expect(dark.cellCrosshairCol).toBe(DATA_GRID_DARK_CROSSHAIR_COL_BG);
|
||||
expect(dark.cellCrosshairRow).not.toBe(dark.cellActive);
|
||||
expect(dark.cellCrosshairCol).not.toBe(dark.cellActive);
|
||||
expect(dark.cellCrosshairCol).not.toBe(dark.cellCrosshairRow);
|
||||
|
||||
// These are spatial location cues rather than text, but they must remain
|
||||
// visibly distinct from the grid surface in both modes.
|
||||
expect(contrastRatio(light.background, light.cellCrosshairRow)).toBeGreaterThanOrEqual(1.5);
|
||||
expect(contrastRatio(light.background, light.cellCrosshairCol)).toBeGreaterThanOrEqual(2);
|
||||
expect(contrastRatio(dark.background, dark.cellCrosshairRow)).toBeGreaterThanOrEqual(1.5);
|
||||
expect(contrastRatio(dark.background, dark.cellCrosshairCol)).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it("lets explicit crosshair CSS variables drive the canvas fills in light mode", () => {
|
||||
const vars: Record<string, string> = {
|
||||
"--data-grid-cell-crosshair-row-bg": "rgb(190, 220, 250)",
|
||||
"--data-grid-cell-crosshair-col-bg": "rgb(160, 205, 250)",
|
||||
};
|
||||
|
||||
const theme = resolveDataGridPaintTheme({ getVar: (name) => vars[name] ?? "", isDark: false });
|
||||
|
||||
expect(theme.cellCrosshairRow).toBe("rgb(190, 220, 250)");
|
||||
expect(theme.cellCrosshairCol).toBe("rgb(160, 205, 250)");
|
||||
});
|
||||
|
||||
it("uses the theme primary color at the same stronger sRGB ratios as the DOM grid", () => {
|
||||
const vars: Record<string, string> = {
|
||||
"--primary": "rgb(58, 123, 106)",
|
||||
"--background": "rgb(248, 250, 248)",
|
||||
"--data-grid-cell-crosshair-row-bg": "color-mix(in srgb, var(--primary) 34%, var(--background))",
|
||||
"--data-grid-cell-crosshair-col-bg": "color-mix(in srgb, var(--primary) 50%, var(--background))",
|
||||
};
|
||||
|
||||
const theme = resolveDataGridPaintTheme({ getVar: (name) => vars[name] ?? "", isDark: false });
|
||||
|
||||
expect(theme.cellCrosshairRow).toBe("rgb(183, 207, 200)");
|
||||
expect(theme.cellCrosshairCol).toBe("rgb(153, 187, 177)");
|
||||
});
|
||||
|
||||
it("propagates crosshair fills through the base cell variable so DOM and frozen cells stay visible", () => {
|
||||
const gridSource = readFileSync(new URL("../../../components/grid/DataGrid.vue", import.meta.url), "utf8");
|
||||
|
||||
expect(gridSource).toMatch(/\.crosshair-row\s*\{\s*--data-grid-cell-bg:\s*var\(--data-grid-cell-crosshair-row-bg\)\s*!important;/);
|
||||
expect(gridSource).toMatch(/\.crosshair-column\s*\{\s*--data-grid-cell-bg:\s*var\(--data-grid-cell-crosshair-col-bg\)\s*!important;/);
|
||||
expect(gridSource).toContain("color-mix(in srgb, var(--primary) 34%, var(--background))");
|
||||
expect(gridSource).toContain("color-mix(in srgb, var(--primary) 50%, var(--background))");
|
||||
expect(gridSource).toMatch(/\.data-grid-cell--frozen\s*\{\s*background-color:\s*var\(--data-grid-cell-bg,/);
|
||||
});
|
||||
|
||||
it("falls back to the built-in type palette for the active appearance", () => {
|
||||
const emptyCssVariable = () => "";
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { dataGridFrameCoversRow, dataGridFrameIsMultiCell, dataGridSelectionFram
|
||||
import type { CellSelectionRange } from "@/lib/dataGrid/gridSelection";
|
||||
import type { RowStatus } from "@/lib/dataGrid/gridRowStatus";
|
||||
import { DATA_GRID_DARK_SEARCH_COLORS, dataGridTypeForeground, resolveDataGridPaintTheme, type DataGridPaintTheme } from "@/lib/dataGrid/dataGridPaintTheme";
|
||||
import type { CrosshairTarget } from "@/lib/dataGrid/crosshairHighlight";
|
||||
|
||||
export const CANVAS_DATA_GRID_ROW_HEIGHT = 26;
|
||||
export const MAX_CANVAS_DATA_GRID_PIXEL_RATIO = 4;
|
||||
@@ -94,6 +95,8 @@ export interface DrawCanvasDataGridOptions {
|
||||
columnAligns?: readonly ("left" | "right")[];
|
||||
columnTypeVisualKinds?: readonly DataGridTypeVisualKind[];
|
||||
colorizeDataTypes?: boolean;
|
||||
/** 行列十字高亮目标(原样传入,null 表示开关关闭或无焦点)。只画当前 viewport 内的行/列底色 */
|
||||
crosshair?: CrosshairTarget | null;
|
||||
rightAlignedActionCell?: CanvasRightAlignedActionCell | null;
|
||||
booleanDisplayMode?: "checkbox" | "dropdown";
|
||||
flatteningMultiLineEnabled: boolean;
|
||||
@@ -353,6 +356,7 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
|
||||
columnAligns,
|
||||
columnTypeVisualKinds,
|
||||
colorizeDataTypes = false,
|
||||
crosshair,
|
||||
rightAlignedActionCell,
|
||||
columnIsBoolean,
|
||||
booleanDisplayMode = "dropdown",
|
||||
@@ -429,6 +433,13 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
|
||||
ctx.fillStyle = rowFill;
|
||||
ctx.fillRect(0, y, width, CANVAS_DATA_GRID_ROW_HEIGHT);
|
||||
|
||||
// 十字行高亮:叠在基础行色之上,但低于整行选中(rowSelectionVisual),
|
||||
// 也低于后续 drawCell 的脏格/搜索/选中格填充
|
||||
if (crosshair?.rowCrosshair && item.displayIndex === crosshair.rowIndex && !rowSelectionVisual && !item.isDeleted) {
|
||||
ctx.fillStyle = theme.cellCrosshairRow;
|
||||
ctx.fillRect(rowNumberWidth, y, width - rowNumberWidth, CANVAS_DATA_GRID_ROW_HEIGHT);
|
||||
}
|
||||
|
||||
// 选区覆盖指示(Navicat 风格):行落在选区范围内时行号淡色高亮;
|
||||
// 优先级低于行选中/状态色/活动行,与 DOM 的级联顺序一致
|
||||
const rowInSelection = !rowSelectionVisual && selectionRowCoverage && dataGridFrameCoversRow(selectionFrames, item.displayIndex);
|
||||
@@ -503,6 +514,12 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
|
||||
const cellPaintWidth = Math.min(width, drawX + colWidth) - clippedX;
|
||||
if (cellPaintWidth <= 0) return;
|
||||
|
||||
// 十字列高亮:整列覆盖,叠在行底色之上;脏格/搜索/选中格填充在其后绘制,优先级更高
|
||||
if (crosshair?.columnCrosshair && visibleColIdx === crosshair.visibleColIdx && !selectedFillVisual && !item.isDeleted) {
|
||||
ctx.fillStyle = theme.cellCrosshairCol;
|
||||
ctx.fillRect(clippedX, y, cellPaintWidth, CANVAS_DATA_GRID_ROW_HEIGHT);
|
||||
}
|
||||
|
||||
if (isDirtyCell && !selectedFillVisual) {
|
||||
ctx.fillStyle = theme.cellDirty;
|
||||
ctx.fillRect(clippedX, y, cellPaintWidth, CANVAS_DATA_GRID_ROW_HEIGHT);
|
||||
@@ -658,6 +675,11 @@ export function drawCanvasDataGrid(options: DrawCanvasDataGridOptions) {
|
||||
ctx.fillStyle = rowFill;
|
||||
ctx.fillRect(rowNumberWidth, y, frozenWidth, CANVAS_DATA_GRID_ROW_HEIGHT);
|
||||
}
|
||||
// 冻结区会重绘底色遮挡第一轮溢入内容,需在此重绘十字行底色保持一致
|
||||
if (crosshair?.rowCrosshair && item.displayIndex === crosshair.rowIndex && !rowSelectionVisual && !item.isDeleted) {
|
||||
ctx.fillStyle = theme.cellCrosshairRow;
|
||||
ctx.fillRect(rowNumberWidth, y, frozenWidth, CANVAS_DATA_GRID_ROW_HEIGHT);
|
||||
}
|
||||
// 绘制冻结列的每个单元格(x 坐标不受 scrollLeft 影响)
|
||||
for (let fcIdx = 0; fcIdx < frozenColumnCount; fcIdx++) {
|
||||
const cellX = rowNumberWidth + (offsets[fcIdx] ?? 0);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { CellPosition } from "@/lib/dataGrid/gridSelection";
|
||||
|
||||
/**
|
||||
* 行列十字高亮目标。只解析「原始选择状态」,不做转置投影(转置是纯展示层映射,
|
||||
* 由 DataGrid 在渲染时把 x/y 轴互换)。供 Canvas / 常规 DOM / 转置 DOM 三个渲染分支共享。
|
||||
*
|
||||
* 坐标约定:
|
||||
* - rowIndex 为原始行(displayIndex);
|
||||
* - visibleColIdx 为可见列下标(selectionFocus.colIndex 语义,与 cellIsSelected 一致);
|
||||
* - actualColIdx 为 visibleColumnIndexes[visibleColIdx] 对应的真实列下标。
|
||||
*/
|
||||
export interface CrosshairTarget {
|
||||
rowIndex: number; // 原始行(displayIndex)
|
||||
visibleColIdx: number; // selectionFocus.colIndex(可见列)
|
||||
actualColIdx: number; // visibleColumnIndexes[visibleColIdx]
|
||||
rowCrosshair: boolean; // 这一轴是否绘制(非"是否焦点格")
|
||||
columnCrosshair: boolean;
|
||||
}
|
||||
|
||||
export interface ResolveCrosshairTargetOptions {
|
||||
selectionFocus: CellPosition | null;
|
||||
selectionAnchor: CellPosition | null;
|
||||
hasRowSelection: boolean;
|
||||
hasColumnSelection: boolean;
|
||||
visibleColumnIndexes: readonly number[];
|
||||
/** 整行选中且无单元格焦点时派生行(DataGrid 从 lastClickedRowIndex / 首个 selectedRowId 推导) */
|
||||
fallbackRowIndex?: number | null;
|
||||
/** 整列选中且无单元格焦点时派生列(DataGrid 从首个 selectedColumnIndexes 推导) */
|
||||
fallbackColumnIndex?: number | null;
|
||||
}
|
||||
|
||||
export function resolveCrosshairTarget(options: ResolveCrosshairTargetOptions): CrosshairTarget | null {
|
||||
const { selectionFocus, hasRowSelection, hasColumnSelection, visibleColumnIndexes } = options;
|
||||
|
||||
if (selectionFocus) {
|
||||
const actualColIdx = visibleColumnIndexes[selectionFocus.colIndex];
|
||||
if (actualColIdx === undefined) return null;
|
||||
// 整行/整列选中会清空 focus(selectRow / selectColumns),因此普通格或多格范围
|
||||
// 的 focus 必不与其他整行/整列选中并存;但防御性处理:若共存,只亮对应轴。
|
||||
const rowCrosshair = !hasColumnSelection;
|
||||
const columnCrosshair = !hasRowSelection;
|
||||
return {
|
||||
rowIndex: selectionFocus.rowIndex,
|
||||
visibleColIdx: selectionFocus.colIndex,
|
||||
actualColIdx,
|
||||
rowCrosshair,
|
||||
columnCrosshair,
|
||||
};
|
||||
}
|
||||
|
||||
// selectRow / handleRowClick 会调用 clearCellSelection() 清空 focus,整行选中时只能派生行。
|
||||
if (hasRowSelection) {
|
||||
if (options.fallbackRowIndex === null || options.fallbackRowIndex === undefined) return null;
|
||||
const actualColIdx = visibleColumnIndexes[0];
|
||||
if (actualColIdx === undefined) return null;
|
||||
return {
|
||||
rowIndex: options.fallbackRowIndex,
|
||||
visibleColIdx: 0,
|
||||
actualColIdx,
|
||||
rowCrosshair: true,
|
||||
columnCrosshair: false,
|
||||
};
|
||||
}
|
||||
|
||||
// selectColumns 会清空 anchor/focus,整列选中时只能派生列。
|
||||
if (hasColumnSelection) {
|
||||
if (options.fallbackColumnIndex === null || options.fallbackColumnIndex === undefined) return null;
|
||||
const actualColIdx = visibleColumnIndexes[options.fallbackColumnIndex];
|
||||
if (actualColIdx === undefined) return null;
|
||||
return {
|
||||
rowIndex: 0,
|
||||
visibleColIdx: options.fallbackColumnIndex,
|
||||
actualColIdx,
|
||||
rowCrosshair: false,
|
||||
columnCrosshair: true,
|
||||
};
|
||||
}
|
||||
|
||||
// 无焦点、加载中、空结果不绘制。
|
||||
return null;
|
||||
}
|
||||
@@ -13,6 +13,8 @@ export interface DataGridPaintTheme {
|
||||
rowNew: string;
|
||||
rowDeleted: string;
|
||||
cellActive: string;
|
||||
cellCrosshairRow: string;
|
||||
cellCrosshairCol: string;
|
||||
cellDirty: string;
|
||||
cellSelected: string;
|
||||
cellSelectedDirty: string;
|
||||
@@ -43,6 +45,17 @@ export const DATA_GRID_DARK_SEARCH_COLORS = {
|
||||
} as const;
|
||||
export const DATA_GRID_LIGHT_ACTIVE_ROW_BG = "rgb(244, 248, 255)";
|
||||
export const DATA_GRID_DARK_ACTIVE_ROW_BG = "rgb(25, 34, 46)";
|
||||
// 十字行/列底色:混入背景的实色(color-mix(in srgb, var(--primary) P%, var(--background))),
|
||||
// 而非透明叠加——铺在浅底上的半透明色会被冲淡到几乎不可见,对比度不足正 jira #7276 的核心痛点。
|
||||
// 行/列占 primary 34%/50%(CSS 方向:P% primary + 其余 background);现有 28%/40% 在浅色
|
||||
// 主题的大面积网格中仍不够醒目。列更重,交点通过 Canvas 的两次绘制进一步加深;选中格仍以显式
|
||||
// 填充与描边优先,故不会被十字高亮覆盖。
|
||||
// 常量按默认主题 soft-light/dark(primary/background)混合得出,作为 var 未设置 / 旧浏览器
|
||||
// 降级实色;color-mix 支持时网格组件的 CSS 变量会用它重新混成 —— 两处取值一致。
|
||||
export const DATA_GRID_LIGHT_CROSSHAIR_ROW_BG = "rgb(174, 195, 224)";
|
||||
export const DATA_GRID_LIGHT_CROSSHAIR_COL_BG = "rgb(142, 170, 210)";
|
||||
export const DATA_GRID_DARK_CROSSHAIR_ROW_BG = "rgb(75, 84, 98)";
|
||||
export const DATA_GRID_DARK_CROSSHAIR_COL_BG = "rgb(98, 111, 130)";
|
||||
export const DATA_GRID_LIGHT_STRIPED_ROW_BG = "rgb(240, 240, 240)";
|
||||
export const DATA_GRID_DARK_STRIPED_ROW_BG = "rgb(40, 40, 43)";
|
||||
export const DATA_GRID_DARK_ROW_NUMBER_BG = "rgb(35, 37, 42)";
|
||||
@@ -151,7 +164,8 @@ function mixRgb(first: RgbaColor, firstPercent: number, second: RgbaColor, secon
|
||||
}
|
||||
|
||||
function parseColorMix(value: string): string | null {
|
||||
const body = value.match(/^color-mix\(\s*in\s+oklab\s*,\s*(.*)\)$/i)?.[1];
|
||||
// Crosshair tokens use sRGB so Canvas can reproduce the DOM blend exactly.
|
||||
const body = value.match(/^color-mix\(\s*in\s+(?:srgb|oklab)\s*,\s*(.*)\)$/i)?.[1];
|
||||
if (!body) return null;
|
||||
const [firstRaw, secondRaw] = splitTopLevelCommas(body);
|
||||
if (!firstRaw || !secondRaw) return null;
|
||||
@@ -257,6 +271,10 @@ export function resolveDataGridPaintTheme(options: { getVar: (name: string) => s
|
||||
const rowNew = isDark ? "rgb(51, 51, 55)" : "rgb(243, 243, 243)";
|
||||
const rowDeleted = isDark ? "rgb(55, 31, 32)" : "rgb(255, 244, 244)";
|
||||
const cellActive = activeSurface;
|
||||
// 浅/深都读 CSS 变量(--data-grid-cell-crosshair-*-bg 从 --primary 派生),保证 DOM 与
|
||||
// Canvas 在不同主题/深浅模式下取到同一色源;固定常量仅作 var 未设置时的降级默认。
|
||||
const cellCrosshairRow = paintToken(getVar, "--data-grid-cell-crosshair-row-bg", isDark ? DATA_GRID_DARK_CROSSHAIR_ROW_BG : DATA_GRID_LIGHT_CROSSHAIR_ROW_BG);
|
||||
const cellCrosshairCol = paintToken(getVar, "--data-grid-cell-crosshair-col-bg", isDark ? DATA_GRID_DARK_CROSSHAIR_COL_BG : DATA_GRID_LIGHT_CROSSHAIR_COL_BG);
|
||||
const cellDirty = isDark ? "rgb(94, 75, 26)" : "rgb(255, 248, 230)";
|
||||
const cellSelected = isDark ? "rgb(20, 40, 60)" : "rgb(239, 246, 255)";
|
||||
const cellSelectedDirty = isDark ? "rgb(76, 66, 38)" : "rgb(235, 224, 184)";
|
||||
@@ -288,6 +306,8 @@ export function resolveDataGridPaintTheme(options: { getVar: (name: string) => s
|
||||
rowNew: isDark ? rowNew : paintToken(getVar, "--data-grid-row-new-bg", rowNew),
|
||||
rowDeleted: isDark ? rowDeleted : paintToken(getVar, "--data-grid-row-deleted-bg", rowDeleted),
|
||||
cellActive: isDark ? cellActive : paintToken(getVar, "--data-grid-cell-active-bg", cellActive),
|
||||
cellCrosshairRow,
|
||||
cellCrosshairCol,
|
||||
cellDirty: paintToken(getVar, "--data-grid-cell-dirty-bg", cellDirty),
|
||||
cellSelected: paintToken(getVar, "--data-grid-cell-selected-bg", cellSelected),
|
||||
cellSelectedDirty: paintToken(getVar, "--data-grid-cell-selected-dirty-bg", cellSelectedDirty),
|
||||
|
||||
@@ -47,6 +47,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
||||
"dataGridTextFilterPanelHeight",
|
||||
"dataGridAutoTransposeSingleRow",
|
||||
"dataGridCellDetailButtonVisible",
|
||||
"dataGridCrosshairHighlight",
|
||||
"pageSize",
|
||||
"tableOpenPageSize",
|
||||
"queryResultMaxRowsEnabled",
|
||||
|
||||
@@ -305,6 +305,16 @@ describe("normalizeEditorSettings", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("defaults the crosshair highlight off and preserves only boolean values", () => {
|
||||
expect(normalizeEditorSettings({}).dataGridCrosshairHighlight).toBe(false);
|
||||
expect(normalizeEditorSettings({ dataGridCrosshairHighlight: true }).dataGridCrosshairHighlight).toBe(true);
|
||||
expect(normalizeEditorSettings({ dataGridCrosshairHighlight: false }).dataGridCrosshairHighlight).toBe(false);
|
||||
|
||||
for (const invalidValue of [0, 1, "true", null]) {
|
||||
expect(normalizeEditorSettings({ dataGridCrosshairHighlight: invalidValue as never }).dataGridCrosshairHighlight).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("defaults the data grid font and preserves a custom font family", () => {
|
||||
const defaultFontFamily = `"Geist Variable Tabular", "Geist Variable", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif`;
|
||||
expect(normalizeEditorSettings({}).tableFontFamily).toBe(defaultFontFamily);
|
||||
@@ -780,6 +790,29 @@ describe("settingsStore persisted settings initialization", () => {
|
||||
expect(restartedStore.editorSettings.dataGridCellDetailButtonVisible).toBe(true);
|
||||
});
|
||||
|
||||
it("defaults the crosshair highlight to off, persists an opt-in, and reloads it", async () => {
|
||||
let persistedSettings: Record<string, unknown> = {};
|
||||
const loadEditorSettings = vi.fn(async () => JSON.parse(JSON.stringify(persistedSettings)));
|
||||
const saveEditorSettings = vi.fn(async (settings: Record<string, unknown>) => {
|
||||
persistedSettings = JSON.parse(JSON.stringify(settings));
|
||||
});
|
||||
vi.doMock("@/lib/backend/api", () => ({ loadEditorSettings, saveEditorSettings }));
|
||||
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const store = useSettingsStore();
|
||||
await store.initEditorSettings();
|
||||
|
||||
expect(store.editorSettings.dataGridCrosshairHighlight).toBe(false);
|
||||
|
||||
await store.updateEditorSettingsAndPersist({ dataGridCrosshairHighlight: true });
|
||||
expect(saveEditorSettings).toHaveBeenLastCalledWith(expect.objectContaining({ dataGridCrosshairHighlight: true }));
|
||||
|
||||
setActivePinia(createPinia());
|
||||
const restartedStore = useSettingsStore();
|
||||
await restartedStore.initEditorSettings();
|
||||
expect(restartedStore.editorSettings.dataGridCrosshairHighlight).toBe(true);
|
||||
});
|
||||
|
||||
it("loads, persists, and reloads hidden query editor line numbers", async () => {
|
||||
let persistedSettings: Record<string, unknown> = { showLineNumbers: true };
|
||||
const loadEditorSettings = vi.fn(async () => JSON.parse(JSON.stringify(persistedSettings)));
|
||||
|
||||
@@ -582,6 +582,7 @@ export interface EditorSettings {
|
||||
resultRunDisplayMode: ResultRunDisplayMode;
|
||||
dataGridAutoTransposeSingleRow: boolean;
|
||||
dataGridCellDetailButtonVisible: boolean;
|
||||
dataGridCrosshairHighlight: boolean;
|
||||
dataGridMultiRowTranspose: boolean;
|
||||
dataGridHideNullColumns: boolean;
|
||||
dataGridBooleanDisplayMode: "dropdown" | "checkbox";
|
||||
@@ -795,6 +796,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
||||
resultRunDisplayMode: "tabs",
|
||||
dataGridAutoTransposeSingleRow: false,
|
||||
dataGridCellDetailButtonVisible: true,
|
||||
dataGridCrosshairHighlight: false,
|
||||
dataGridMultiRowTranspose: false,
|
||||
dataGridHideNullColumns: false,
|
||||
dataGridBooleanDisplayMode: "dropdown",
|
||||
@@ -1172,6 +1174,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
||||
resultRunDisplayMode: normalizeResultRunDisplayMode(settings.resultRunDisplayMode),
|
||||
dataGridAutoTransposeSingleRow: settings.dataGridAutoTransposeSingleRow === true,
|
||||
dataGridCellDetailButtonVisible: typeof settings.dataGridCellDetailButtonVisible === "boolean" ? settings.dataGridCellDetailButtonVisible : DEFAULT_EDITOR_SETTINGS.dataGridCellDetailButtonVisible,
|
||||
dataGridCrosshairHighlight: typeof settings.dataGridCrosshairHighlight === "boolean" ? settings.dataGridCrosshairHighlight : DEFAULT_EDITOR_SETTINGS.dataGridCrosshairHighlight,
|
||||
dataGridMultiRowTranspose: settings.dataGridMultiRowTranspose === true,
|
||||
dataGridHideNullColumns: settings.dataGridHideNullColumns === true,
|
||||
dataGridBooleanDisplayMode: settings.dataGridBooleanDisplayMode === "checkbox" ? "checkbox" : "dropdown",
|
||||
@@ -1788,6 +1791,7 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
if (partial.resultRunDisplayMode !== undefined) editorSettings.value.resultRunDisplayMode = normalizeResultRunDisplayMode(partial.resultRunDisplayMode);
|
||||
if (partial.dataGridAutoTransposeSingleRow !== undefined) editorSettings.value.dataGridAutoTransposeSingleRow = partial.dataGridAutoTransposeSingleRow === true;
|
||||
if (partial.dataGridCellDetailButtonVisible !== undefined) editorSettings.value.dataGridCellDetailButtonVisible = typeof partial.dataGridCellDetailButtonVisible === "boolean" ? partial.dataGridCellDetailButtonVisible : DEFAULT_EDITOR_SETTINGS.dataGridCellDetailButtonVisible;
|
||||
if (partial.dataGridCrosshairHighlight !== undefined) editorSettings.value.dataGridCrosshairHighlight = typeof partial.dataGridCrosshairHighlight === "boolean" ? partial.dataGridCrosshairHighlight : DEFAULT_EDITOR_SETTINGS.dataGridCrosshairHighlight;
|
||||
if (partial.dataGridMultiRowTranspose !== undefined) editorSettings.value.dataGridMultiRowTranspose = partial.dataGridMultiRowTranspose === true;
|
||||
if (partial.dataGridHideNullColumns !== undefined) editorSettings.value.dataGridHideNullColumns = partial.dataGridHideNullColumns === true;
|
||||
if (partial.dataGridBooleanDisplayMode !== undefined) editorSettings.value.dataGridBooleanDisplayMode = partial.dataGridBooleanDisplayMode === "dropdown" ? "dropdown" : "checkbox";
|
||||
|
||||
Reference in new issue
Block a user