fix(ui): 优化工具栏和历史筛选区溢出表现

Co-authored-by: zipg <4047349+zipg@users.noreply.github.com>
This commit is contained in:
zipg 2026-07-07 21:42:44 +08:00 committed by GitHub
parent 42eaddac45
commit bcabce3686
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 134 additions and 28 deletions

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, onMounted } from "vue";
import { ref, computed, nextTick, onBeforeUnmount, onMounted, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
import { CalendarClock, Copy, Database, RotateCcw, Search, Sparkles, Trash2, X } from "@lucide/vue";
@ -43,6 +43,9 @@ const isRollingBack = ref(false);
const showDeleteConfirm = ref(false);
const showClearConfirm = ref(false);
const deleteTargetId = ref<string | null>(null);
const filterScrollRef = ref<HTMLElement | null>(null);
const filtersScrollable = ref(false);
let filterScrollResizeObserver: ResizeObserver | null = null;
const filters: HistoryFilter[] = ["all", "query", "data_change", "schema_change", "failed"];
const hasDateFilter = computed(() => hasHistoryDateRange(dateRange.value));
@ -139,6 +142,16 @@ function filterLabel(filter: HistoryFilter) {
return t(`history.filters.${filter}`);
}
function updateFilterScrollability() {
const el = filterScrollRef.value;
if (!el) return;
const scrollable = el.scrollWidth > el.clientWidth + 3;
filtersScrollable.value = scrollable;
if (!scrollable && el.scrollLeft !== 0) {
el.scrollLeft = 0;
}
}
function openDateRangeFilter() {
dateRangeDraft.value = { ...dateRange.value };
}
@ -252,7 +265,27 @@ function getHistoryMenuItems(entry: HistoryEntry): ContextMenuItem[] {
];
}
onMounted(() => store.load());
watch(
() => filters.map((filter) => filterLabel(filter)).join("\0"),
() => {
void nextTick(updateFilterScrollability);
},
);
onMounted(() => {
store.load();
void nextTick(updateFilterScrollability);
if (filterScrollRef.value) {
filterScrollResizeObserver = new ResizeObserver(updateFilterScrollability);
filterScrollResizeObserver.observe(filterScrollRef.value);
}
});
onBeforeUnmount(() => {
filterScrollResizeObserver?.disconnect();
filterScrollResizeObserver = null;
});
</script>
<template>
@ -269,7 +302,7 @@ onMounted(() => store.load());
</div>
<div class="border-b shrink-0">
<div class="flex gap-1 overflow-x-auto px-2 pt-2">
<div ref="filterScrollRef" class="history-filter-scroll flex gap-1 px-2 pt-2" :class="{ 'history-filter-scroll--scrollable': filtersScrollable }">
<button v-for="filter in filters" :key="filter" type="button" class="h-6 shrink-0 rounded border px-2 text-xs" :class="activeFilter === filter ? 'border-primary bg-primary text-primary-foreground' : 'bg-background'" @click="activeFilter = filter">
{{ filterLabel(filter) }}
</button>
@ -454,3 +487,30 @@ onMounted(() => store.load());
</Dialog>
</div>
</template>
<style scoped>
.history-filter-scroll {
overflow-x: hidden;
overscroll-behavior-x: none;
}
.history-filter-scroll--scrollable {
overflow-x: auto;
padding-bottom: 2px;
scrollbar-color: color-mix(in oklab, var(--muted-foreground) 45%, transparent) transparent;
scrollbar-width: thin;
}
.history-filter-scroll--scrollable::-webkit-scrollbar {
height: 4px;
}
.history-filter-scroll--scrollable::-webkit-scrollbar-track {
background: transparent;
}
.history-filter-scroll--scrollable::-webkit-scrollbar-thumb {
border-radius: 999px;
background: color-mix(in oklab, var(--muted-foreground) 45%, transparent);
}
</style>

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref, onMounted, onBeforeUnmount, h } from "vue";
import { computed, ref, onMounted, onBeforeUnmount, h, nextTick, watch } from "vue";
import { useI18n } from "vue-i18n";
import { DatabaseZap, FilePlus2, Loader2, Moon, Sun, SunMoon, History, Bot, ArrowLeftRight, FileCode, BookMarked, GitCompareArrows, TableProperties, Settings, CloudDownload, Package, FileDown, FolderTree } from "@lucide/vue";
import { Button } from "@/components/ui/button";
@ -61,6 +61,7 @@ const { toast } = useToast();
const settingsStore = useSettingsStore();
const toolbarItems = computed(() => settingsStore.editorSettings.toolbarItems);
const { isMac, isDesktop, showControls, isMaximized, isFullscreen, minimize, toggleMaximize, close } = useWindowControls();
const checkingUpdates = computed(() => props.checkingUpdates);
const themeTriggerIcon = computed(() => {
if (isSystemAppThemeMode(props.themeMode)) return SunMoon;
@ -108,7 +109,9 @@ function checkToolbarWidth() {
const rightWrapper = ref<HTMLElement>();
const rightOverflowCount = ref(0);
let prevRightAvailable = 0;
let toolbarLayoutRaf = 0;
let settlingRightOverflow = false;
let pendingRightOverflowSettle = false;
/** Ordered list of right-side item keys that can overflow into "More".
* Items earlier in the list overflow first when space shrinks. */
@ -212,45 +215,91 @@ const overflowRightMenuItems = computed(() => {
}));
});
function checkRightOverflow() {
async function settleRightOverflowOnce() {
const wrapper = rightWrapper.value;
if (!wrapper) return;
const available = wrapper.clientWidth;
const growing = available > prevRightAvailable + 1;
prevRightAvailable = available;
const defsLength = collapsibleRightItemDefs.value.length;
if (rightOverflowCount.value > defsLength) {
rightOverflowCount.value = defsLength;
await nextTick();
}
if (wrapper.scrollWidth > wrapper.clientWidth) {
// Overflow move one more item to "More" (always, even when growing
// brought an item back that still doesn't fit)
if (rightOverflowCount.value < collapsibleRightItemDefs.value.length) {
for (let i = 0; i <= defsLength + 1; i++) {
const current = rightWrapper.value;
if (!current) return;
if (current.scrollWidth > current.clientWidth + 1 && rightOverflowCount.value < defsLength) {
rightOverflowCount.value++;
await nextTick();
continue;
}
} else if (growing && rightOverflowCount.value > 0) {
// Window growing try bringing one item back
if (rightOverflowCount.value <= 0) return;
rightOverflowCount.value--;
await nextTick();
const restored = rightWrapper.value;
if (restored && restored.scrollWidth <= restored.clientWidth + 1) {
continue;
}
rightOverflowCount.value++;
await nextTick();
return;
}
}
async function settleRightOverflow() {
if (settlingRightOverflow) {
pendingRightOverflowSettle = true;
return;
}
settlingRightOverflow = true;
try {
do {
pendingRightOverflowSettle = false;
await nextTick();
await settleRightOverflowOnce();
} while (pendingRightOverflowSettle);
} finally {
settlingRightOverflow = false;
}
}
function scheduleToolbarLayout() {
if (toolbarLayoutRaf) cancelAnimationFrame(toolbarLayoutRaf);
toolbarLayoutRaf = requestAnimationFrame(() => {
toolbarLayoutRaf = 0;
checkToolbarWidth();
void settleRightOverflow();
});
}
function handleWindowResize() {
scheduleToolbarLayout();
}
watch(collapsibleRightItemDefs, () => scheduleToolbarLayout(), { flush: "post" });
// Resize observer
let resizeObserver: ResizeObserver | null = null;
onMounted(() => {
resizeObserver = new ResizeObserver(() => {
checkToolbarWidth();
checkRightOverflow();
});
resizeObserver = new ResizeObserver(scheduleToolbarLayout);
if (toolbarEl.value) resizeObserver.observe(toolbarEl.value);
window.addEventListener("resize", () => {
checkToolbarWidth();
checkRightOverflow();
});
if (rightWrapper.value) resizeObserver.observe(rightWrapper.value);
window.addEventListener("resize", handleWindowResize);
scheduleToolbarLayout();
});
onBeforeUnmount(() => {
if (toolbarLayoutRaf) cancelAnimationFrame(toolbarLayoutRaf);
resizeObserver?.disconnect();
window.removeEventListener("resize", checkToolbarWidth);
window.removeEventListener("resize", handleWindowResize);
});
// Left-side "More" items
@ -365,9 +414,6 @@ function isRightItemVisible(key: string) {
return !overflowedRightKeys.value.has(key);
}
// Track checking updates state for the overflow menu disabled state
const checkingUpdates = computed(() => props.checkingUpdates);
const toolbarTextButtonClass = "h-8 px-2 text-xs gap-1 leading-none";
const toolbarTextLabelClass = "inline-flex translate-y-px items-center leading-none";
const toolbarDropdownTriggerClass = `inline-flex h-8 items-center gap-1 rounded-[6px] px-2 text-xs font-medium leading-none hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 transition-colors [&>span:first-child]:translate-y-px`;
@ -432,7 +478,7 @@ const toolbarDropdownTriggerClass = `inline-flex h-8 items-center gap-1 rounded-
<div class="flex-1" data-tauri-drag-region />
<!-- Right-side items wrapped in overflow-aware container -->
<div ref="rightWrapper" class="flex items-center gap-1 overflow-hidden">
<div ref="rightWrapper" class="flex min-w-0 items-center gap-1 overflow-hidden">
<template v-if="toolbarItems.checkUpdates">
<Tooltip>
<TooltipTrigger as-child>