fix(dashboard): stabilize editable layout controls

This commit is contained in:
jxxghp
2026-06-06 18:22:36 +08:00
parent e2722801e4
commit 75da7d35b4
14 changed files with 295 additions and 307 deletions

View File

@@ -33,6 +33,7 @@ const DASHBOARD_GRID_COLUMNS = 12
const DASHBOARD_GRID_CELL_HEIGHT = 16
const DASHBOARD_GRID_FALLBACK_ROWS = 4
const DASHBOARD_GRID_MARGIN = 8
const DASHBOARD_GRID_CONTENT_RESIZE_THRESHOLD = 4
const DASHBOARD_GRID_LAYOUT_STORAGE_KEY = 'MP_DASHBOARD_GRID_LAYOUT'
interface DashboardGridLayoutItem {
@@ -66,8 +67,12 @@ const isSyncingDashboardGrid = ref(false)
// 仪表板本地布局覆盖配置
const dashboardGridLayout = ref<Record<string, DashboardGridLayoutItem>>({})
// 是否刚恢复过默认布局,用于避免退出编辑时立即把默认布局写回本地覆盖。
const isDashboardGridLayoutResetPending = ref(false)
const dashboardGridResizeStartHeights = new Map<string, number | undefined>()
const dashboardGridPendingContentResize = new Set<GridItemHTMLElement>()
const dashboardGridObservedContentHeights = new Map<string, number>()
let dashboardGridContentObserver: ResizeObserver | null = null
let dashboardGridContentResizeFrame: number | null = null
@@ -324,7 +329,6 @@ function openDashboardSettings() {
hint: t('dashboard.chooseContent'),
items: dashboardConfigs.value,
labelGetter: (item: DashboardItem) => item.attrs?.title ?? item.name,
resetText: t('dashboard.resetLayout'),
title: t('dashboard.settings'),
valueGetter: (item: DashboardItem) => buildPluginDashboardId(item.id, item.key),
},
@@ -332,7 +336,6 @@ function openDashboardSettings() {
close: () => {
settingsDialogController = null
},
reset: resetDashboardGridLayout,
save: saveDashboardConfig,
'update:modelValue': (value: boolean) => {
if (!value) settingsDialogController = null
@@ -347,6 +350,7 @@ function resetDashboardGridLayout() {
dashboardGridLayout.value = {}
localStorage.removeItem(DASHBOARD_GRID_LAYOUT_STORAGE_KEY)
dashboardGrid.value?.removeAll(false, false)
isDashboardGridLayoutResetPending.value = true
nextTick(syncDashboardGrid)
}
@@ -354,20 +358,32 @@ function resetDashboardGridLayout() {
const dashboardDynamicButtonMenuItems = computed<DynamicButtonMenuItem[] | undefined>(() => {
if (!appMode.value) return undefined
return [
const items: DynamicButtonMenuItem[] = [
{
title: isLayoutEditing.value ? t('dashboard.exitEditMode') : t('dashboard.editLayout'),
icon: isLayoutEditing.value ? 'mdi-check' : 'mdi-view-dashboard-edit',
color: 'primary',
action: toggleDashboardLayoutEditing,
},
{
title: t('dashboard.settings'),
icon: 'mdi-tune',
color: 'info',
action: openDashboardSettings,
},
]
if (isLayoutEditing.value) {
items.push({
title: t('dashboard.resetLayout'),
icon: 'mdi-restore',
color: 'warning',
action: resetDashboardGridLayout,
})
}
items.push({
title: t('dashboard.settings'),
icon: 'mdi-tune',
color: 'info',
action: openDashboardSettings,
})
return items
})
useDynamicButton({
@@ -379,11 +395,16 @@ useDynamicButton({
// 切换仪表板布局编辑模式,退出编辑时压实并保存当前布局。
function toggleDashboardLayoutEditing() {
if (isLayoutEditing.value) {
compactAndPersistDashboardGrid()
if (isDashboardGridLayoutResetPending.value) {
isDashboardGridLayoutResetPending.value = false
} else {
compactAndPersistDashboardGrid()
}
isLayoutEditing.value = false
return
}
isDashboardGridLayoutResetPending.value = false
isLayoutEditing.value = true
nextTick(syncDashboardGrid)
}
@@ -676,10 +697,14 @@ function observeDashboardGridContent() {
if (!gridElement || typeof ResizeObserver === 'undefined') return
dashboardGridContentObserver?.disconnect()
dashboardGridPendingContentResize.clear()
dashboardGridObservedContentHeights.clear()
dashboardGridContentObserver = new ResizeObserver(entries => {
entries.forEach(entry => {
const itemElement = entry.target.closest('.dashboard-grid-item') as GridItemHTMLElement | null
if (itemElement) scheduleDashboardItemContentResize(itemElement)
if (itemElement && shouldScheduleDashboardContentResize(itemElement, entry.contentRect.height)) {
scheduleDashboardItemContentResize(itemElement)
}
})
})
@@ -688,6 +713,20 @@ function observeDashboardGridContent() {
})
}
// 判断内容高度变化是否足够触发 GridStack 行高重算,避免 hover 级微小波动造成布局抖动。
function shouldScheduleDashboardContentResize(element: GridItemHTMLElement, nextHeight: number) {
const id = element.getAttribute('gs-id') ?? ''
if (!id) return true
const previousHeight = dashboardGridObservedContentHeights.get(id)
dashboardGridObservedContentHeights.set(id, nextHeight)
return (
previousHeight === undefined ||
Math.abs(nextHeight - previousHeight) >= DASHBOARD_GRID_CONTENT_RESIZE_THRESHOLD
)
}
// 延迟执行单个组件内容测高,合并连续 ResizeObserver 回调。
function scheduleDashboardItemContentResize(element: GridItemHTMLElement) {
dashboardGridPendingContentResize.add(element)
@@ -805,6 +844,7 @@ function getDefaultDashboardGridWidthById(id: string) {
function compactAndPersistDashboardGrid(manualHeightId: string | false = false) {
if (!dashboardGrid.value || isSyncingDashboardGrid.value) return
isDashboardGridLayoutResetPending.value = false
dashboardGrid.value.compact('compact')
nextTick(() => persistDashboardGridLayout(manualHeightId))
}
@@ -855,6 +895,7 @@ onBeforeUnmount(() => {
dashboardGridResizeRefreshFrame = null
}
dashboardGridPendingContentResize.clear()
dashboardGridObservedContentHeights.clear()
dashboardGridResizeStartHeights.clear()
dashboardGrid.value?.destroy(false)
dashboardGrid.value = null
@@ -905,6 +946,15 @@ onBeforeUnmount(() => {
class="compact-fab compact-fab--secondary"
@click="openDashboardSettings"
/>
<VFab
v-if="isLayoutEditing"
icon="mdi-restore"
color="warning"
variant="tonal"
appear
class="compact-fab compact-fab--secondary"
@click="resetDashboardGridLayout"
/>
<VFab
:icon="isLayoutEditing ? 'mdi-check' : 'mdi-view-dashboard-edit'"
color="primary"