feat(grid): 添加多行转置功能和相关界面支持 (#909)
- 新增多行转置状态管理 - 更新转置记录索引计算逻辑 - 增加多行转置切换开关及提示信息 - 更新国际化文件以支持多语言
This commit is contained in:
11 files changed
+172
-13
No files matched your search
@@ -105,6 +105,7 @@ import {
|
||||
nextKeyboardTransposeState,
|
||||
nextTransposeState,
|
||||
nextTransposeStateForRecordCount,
|
||||
transposeRecordIndexesForMode,
|
||||
transposeFieldWidth,
|
||||
transposeScrollLeftForRecord,
|
||||
visibleTransposeRecordWindow,
|
||||
@@ -460,6 +461,7 @@ const previewDialogOpen = ref(false);
|
||||
const previewDialogConfig = shallowRef<{ component: any; props: Record<string, any> } | null>(null);
|
||||
const transposeRowIndex = ref<number | null>(null);
|
||||
const showTranspose = ref(false);
|
||||
const multiRowTranspose = ref(false);
|
||||
const preserveTransposeOnNextResult = ref(false);
|
||||
const transposeScrollRef = ref<HTMLElement | { $el?: HTMLElement }>();
|
||||
const transposeScrollLeft = ref(0);
|
||||
@@ -4836,22 +4838,37 @@ const visibleTransposeRecordIndexes = computed(() => {
|
||||
const window = transposeRecordWindow.value;
|
||||
return Array.from({ length: window.end - window.start }, (_, offset) => window.start + offset);
|
||||
});
|
||||
const activeTransposeRecordIndexes = computed(() =>
|
||||
transposeRecordIndexesForMode({
|
||||
multiRow: multiRowTranspose.value,
|
||||
activeRecordIndex: transposeRowIndex.value,
|
||||
totalRecords: displayRowCount.value,
|
||||
visibleRecordIndexes: visibleTransposeRecordIndexes.value,
|
||||
}),
|
||||
);
|
||||
const transposeBeforeSpacerWidth = computed(() =>
|
||||
multiRowTranspose.value ? transposeRecordWindow.value.beforeWidth : 0,
|
||||
);
|
||||
const transposeAfterSpacerWidth = computed(() =>
|
||||
multiRowTranspose.value ? transposeRecordWindow.value.afterWidth : 0,
|
||||
);
|
||||
const transposeRows = computed(() => {
|
||||
return buildVisibleTransposeRows({
|
||||
columns: visibleColumns.value,
|
||||
records: displayRowRefs.value.map((_, index) => displayItemAt(index)?.data ?? []),
|
||||
recordIndexes: visibleTransposeRecordIndexes.value,
|
||||
recordIndexes: activeTransposeRecordIndexes.value,
|
||||
valueIndexes: visibleColumnIndexes.value,
|
||||
typeByColumn: columnTypeMap.value,
|
||||
displayValue: (value, _column, index) => formatCellCached(value, visibleColumnIndexes.value[index]),
|
||||
});
|
||||
});
|
||||
const isTransposeMode = computed(() => showTranspose.value && transposeRows.value.length > 0);
|
||||
const transposeTotalWidth = computed(
|
||||
() =>
|
||||
transposePinnedWidth.value +
|
||||
Array.from({ length: displayRowCount.value }, (_, i) => getTransposeRecordWidth(i)).reduce((sum, w) => sum + w, 0),
|
||||
);
|
||||
const transposeTotalWidth = computed(() => {
|
||||
const recordIndexes = multiRowTranspose.value
|
||||
? Array.from({ length: displayRowCount.value }, (_, i) => i)
|
||||
: activeTransposeRecordIndexes.value;
|
||||
return transposePinnedWidth.value + recordIndexes.reduce((sum, i) => sum + getTransposeRecordWidth(i), 0);
|
||||
});
|
||||
|
||||
function transposeScrollElement(): HTMLElement | undefined {
|
||||
const raw = transposeScrollRef.value;
|
||||
@@ -4888,6 +4905,26 @@ function scrollTransposeRecordIntoView(rowIndex: number) {
|
||||
});
|
||||
}
|
||||
|
||||
function setMultiRowTranspose(value: boolean) {
|
||||
multiRowTranspose.value = value;
|
||||
if (!showTranspose.value) return;
|
||||
nextTick(updateTransposeViewport);
|
||||
if (value && transposeRowIndex.value !== null) {
|
||||
scrollTransposeRecordIntoView(transposeRowIndex.value);
|
||||
} else {
|
||||
nextTick(() => {
|
||||
const el = transposeScrollElement();
|
||||
if (!el) return;
|
||||
el.scrollLeft = 0;
|
||||
updateTransposeViewport();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMultiRowTranspose() {
|
||||
setMultiRowTranspose(!multiRowTranspose.value);
|
||||
}
|
||||
|
||||
function applyTransposeState(next: { showTranspose: boolean; transposeRowIndex: number | null }) {
|
||||
showTranspose.value = next.showTranspose;
|
||||
transposeRowIndex.value = next.transposeRowIndex;
|
||||
@@ -5624,6 +5661,9 @@ defineExpose({
|
||||
toggleDdl: toggleTableInfo,
|
||||
showTableInfo,
|
||||
toggleTableInfo,
|
||||
multiRowTranspose,
|
||||
setMultiRowTranspose,
|
||||
toggleMultiRowTranspose,
|
||||
focusSearch,
|
||||
visibleColumnCount,
|
||||
displayableColumnCount,
|
||||
@@ -6436,6 +6476,11 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ t("grid.rowNumber") }} {{ (transposeRowIndex ?? 0) + 1 }}
|
||||
</span>
|
||||
<span
|
||||
class="rounded border border-border bg-background px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground"
|
||||
>
|
||||
{{ multiRowTranspose ? t("grid.transposeMultiRow") : t("grid.transposeSingleRow") }}
|
||||
</span>
|
||||
<span class="flex-1" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -6488,9 +6533,9 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
@mousedown.stop="onTransposePinnedResizeStart"
|
||||
/>
|
||||
</div>
|
||||
<div class="shrink-0" :style="{ width: `${transposeRecordWindow.beforeWidth}px` }" />
|
||||
<div class="shrink-0" :style="{ width: `${transposeBeforeSpacerWidth}px` }" />
|
||||
<div
|
||||
v-for="recordIndex in visibleTransposeRecordIndexes"
|
||||
v-for="recordIndex in activeTransposeRecordIndexes"
|
||||
:key="`transpose-head-${recordIndex}`"
|
||||
class="shrink-0 border-r border-border px-2 py-1.5 text-center tabular-nums relative"
|
||||
:class="{
|
||||
@@ -6514,7 +6559,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
@dblclick.stop="autoFitTransposeRecord(recordIndex)"
|
||||
/>
|
||||
</div>
|
||||
<div class="shrink-0" :style="{ width: `${transposeRecordWindow.afterWidth}px` }" />
|
||||
<div class="shrink-0" :style="{ width: `${transposeAfterSpacerWidth}px` }" />
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ item }">
|
||||
@@ -6529,7 +6574,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
>
|
||||
{{ item.column }}
|
||||
</div>
|
||||
<div class="shrink-0" :style="{ width: `${transposeRecordWindow.beforeWidth}px` }" />
|
||||
<div class="shrink-0" :style="{ width: `${transposeBeforeSpacerWidth}px` }" />
|
||||
<div
|
||||
v-for="cell in item.values"
|
||||
:key="`${item.id}:${cell.recordIndex}`"
|
||||
@@ -6647,7 +6692,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="shrink-0" :style="{ width: `${transposeRecordWindow.afterWidth}px` }" />
|
||||
<div class="shrink-0" :style="{ width: `${transposeAfterSpacerWidth}px` }" />
|
||||
</div>
|
||||
</template>
|
||||
</RecycleScroller>
|
||||
|
||||
@@ -23,6 +23,8 @@ import { Splitpanes, Pane } from "splitpanes";
|
||||
import "splitpanes/dist/splitpanes.css";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import LightTooltip from "@/components/ui/LightTooltip.vue";
|
||||
import QueryEditor from "@/components/editor/QueryEditor.vue";
|
||||
import ColumnInfoPanel from "@/components/editor/ColumnInfoPanel.vue";
|
||||
import type { ColumnInfo } from "@/components/editor/ColumnInfoPanel.vue";
|
||||
@@ -81,6 +83,8 @@ type DataGridHandle = {
|
||||
toggleAllNullColumns: () => void;
|
||||
showDdl: boolean;
|
||||
toggleDdl: () => void;
|
||||
multiRowTranspose: boolean;
|
||||
setMultiRowTranspose: (value: boolean) => void;
|
||||
};
|
||||
|
||||
type SearchableBrowserHandle = {
|
||||
@@ -864,7 +868,9 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-7 shrink-0 text-foreground hover:bg-accent"
|
||||
:class="{ 'bg-accent text-foreground': dataGridRef?.nullColumnsHidden }"
|
||||
:class="{
|
||||
'bg-accent text-foreground': dataGridRef?.nullColumnsHidden || dataGridRef?.multiRowTranspose,
|
||||
}"
|
||||
:title="t('grid.viewOptions')"
|
||||
:aria-label="t('grid.viewOptions')"
|
||||
>
|
||||
@@ -880,6 +886,28 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
|
||||
<div class="border-b bg-muted/40 px-3 py-2">
|
||||
<div class="text-xs font-semibold">{{ t("grid.viewOptions") }}</div>
|
||||
</div>
|
||||
<LightTooltip
|
||||
:text="t('grid.transposeMultiRowHint')"
|
||||
side="left"
|
||||
:side-offset="6"
|
||||
:delay="0"
|
||||
:open-on-focus="false"
|
||||
>
|
||||
<label class="flex cursor-pointer items-center justify-between gap-3 px-3 py-2 text-xs hover:bg-accent">
|
||||
<span class="min-w-0 flex items-center gap-1.5 font-medium">
|
||||
{{ t("grid.transposeMultiRowToggle") }}
|
||||
<span class="text-muted-foreground">
|
||||
{{ dataGridRef?.multiRowTranspose ? t("grid.transposeMultiRow") : t("grid.transposeSingleRow") }}
|
||||
</span>
|
||||
</span>
|
||||
<Switch
|
||||
size="sm"
|
||||
:model-value="!!dataGridRef?.multiRowTranspose"
|
||||
:aria-label="t('grid.transposeMultiRow')"
|
||||
@update:model-value="(value: boolean) => dataGridRef?.setMultiRowTranspose(value)"
|
||||
/>
|
||||
</label>
|
||||
</LightTooltip>
|
||||
<label
|
||||
class="flex cursor-pointer items-center gap-2 px-3 py-2 text-xs hover:bg-accent"
|
||||
:class="{ 'cursor-not-allowed opacity-60': !dataGridRef?.canToggleAllNullColumns }"
|
||||
|
||||
@@ -8,12 +8,14 @@ const props = withDefaults(
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
sideOffset?: number;
|
||||
delay?: number;
|
||||
openOnFocus?: boolean;
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
side: "top",
|
||||
sideOffset: 8,
|
||||
delay: 300,
|
||||
openOnFocus: true,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -147,6 +149,11 @@ function scheduleOpen() {
|
||||
timer = setTimeout(open, props.delay);
|
||||
}
|
||||
|
||||
function scheduleFocusOpen() {
|
||||
if (!props.openOnFocus) return;
|
||||
scheduleOpen();
|
||||
}
|
||||
|
||||
onBeforeUnmount(close);
|
||||
|
||||
watch(
|
||||
@@ -164,7 +171,7 @@ watch(
|
||||
class="contents"
|
||||
@mouseenter="scheduleOpen"
|
||||
@mouseleave="close"
|
||||
@focusin="scheduleOpen"
|
||||
@focusin="scheduleFocusOpen"
|
||||
@focusout="close"
|
||||
>
|
||||
<slot />
|
||||
|
||||
Reference in new issue
Block a user