Skip to content

Commit 200d568

Browse files
committed
feat(ui): 完成账号广场视口布局与自适应分页
1 parent 5483090 commit 200d568

8 files changed

Lines changed: 692 additions & 284 deletions

File tree

docs/site/content/docs/operations/changelog.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ description: 按版本记录 Pixel API 的主要功能更新。
55

66
## v1.2.69
77

8+
- 账号广场按窗口高度安排筛选栏、卡片和底部分页,减少两侧留白,并按可用空间调整每页卡片数量。
89
- 修复账号状态更新在 PostgreSQL 中的参数类型冲突,保持首次错误时间、重复错误和恢复状态的处理规则。
910
- 修复代理创建后立即更新时因时间精度差异被误报为数据已变更的问题,继续使用数据库中的更新时间校验并发修改。
1011
- 调度缓存重建仅读取活跃分组 ID,减少完整分组对象的加载,并记录各阶段耗时以便定位重建瓶颈。

frontend/src/components/account-share/MembershipHistoryPanel.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@
294294
</div>
295295

296296
<Pagination
297-
v-if="!loading && total > pageSize"
297+
v-if="showPagination && !loading && total > pageSize"
298298
class="overflow-hidden rounded-xl border border-slate-200 shadow-sm dark:border-dark-700"
299299
:page="page"
300300
:total="total"
@@ -311,14 +311,15 @@ import Icon from '@/components/icons/Icon.vue'
311311
import Pagination from '@/components/common/Pagination.vue'
312312
import HistoryTerm from './MembershipHistoryTerm.vue'
313313
314-
defineProps<{
314+
withDefaults(defineProps<{
315315
items: AccountShareMembershipHistoryEntry[]
316316
loading: boolean
317317
errorMessage: string
318318
page: number
319319
pageSize: number
320320
total: number
321-
}>()
321+
showPagination?: boolean
322+
}>(), { showPagination: true })
322323
323324
const emit = defineEmits<{
324325
reload: []
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { onScopeDispose, watch, type Ref } from 'vue'
2+
3+
interface RoomGridCapacityOptions {
4+
viewport: Ref<HTMLElement | null>
5+
grid: Ref<HTMLElement | null>
6+
enabled: Readonly<Ref<boolean>>
7+
onCapacityChange: (size: number) => void
8+
}
9+
10+
const MEASUREMENT_DELAY_MS = 120
11+
const MAX_PAGE_SIZE = 1000
12+
13+
export function useRoomGridCapacity({ viewport, grid, enabled, onCapacityChange }: RoomGridCapacityOptions): void {
14+
if (typeof window === 'undefined' || typeof ResizeObserver === 'undefined') return
15+
16+
let timer: number | null = null
17+
let disposed = false
18+
let heightBaseline = ''
19+
let maximumCardHeight = 0
20+
let lastMeasurement = ''
21+
let lastCapacity = 0
22+
23+
function cancelMeasurement(): void {
24+
if (timer !== null) window.clearTimeout(timer)
25+
timer = null
26+
}
27+
28+
function measure(): void {
29+
timer = null
30+
const viewportElement = viewport.value
31+
const gridElement = grid.value
32+
if (disposed || !enabled.value || !viewportElement || !gridElement) return
33+
34+
const width = viewportElement.clientWidth
35+
const height = viewportElement.clientHeight
36+
const gridWidth = gridElement.clientWidth
37+
if (width <= 0 || height <= 0 || gridWidth <= 0 || gridElement.clientHeight <= 0) return
38+
39+
const style = window.getComputedStyle(gridElement)
40+
if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse') return
41+
// 浏览器将 repeat()/minmax() 展开为像素轨道;不推测尚未完成布局的 CSS 表达式。
42+
const tracks = style.gridTemplateColumns.replace(/\[[^\]]*\]/g, '').trim().split(/\s+/)
43+
if (tracks.length === 0 || tracks.some(track => !/^\d+(?:\.\d+)?px$/.test(track))) return
44+
const columns = tracks.filter(track => Number.parseFloat(track) > 0).length
45+
if (columns === 0) return
46+
47+
let measuredCardHeight = 0
48+
for (const card of gridElement.querySelectorAll<HTMLElement>('.room-preview-card')) {
49+
measuredCardHeight = Math.max(measuredCardHeight, Math.ceil(card.getBoundingClientRect().height))
50+
}
51+
if (measuredCardHeight <= 0) return
52+
53+
const fontSize = Number.parseFloat(style.fontSize)
54+
const rootFontSize = Number.parseFloat(window.getComputedStyle(document.documentElement).fontSize)
55+
if (!Number.isFinite(fontSize) || fontSize <= 0 || !Number.isFinite(rootFontSize) || rootFontSize <= 0) return
56+
const nextBaseline = `${width}:${gridWidth}:${fontSize}:${rootFontSize}`
57+
if (nextBaseline !== heightBaseline) {
58+
heightBaseline = nextBaseline
59+
maximumCardHeight = measuredCardHeight
60+
} else {
61+
// 同一宽度与字体尺度保留已见最高卡片,避免短页导致容量来回跳变。
62+
maximumCardHeight = Math.max(maximumCardHeight, measuredCardHeight)
63+
}
64+
65+
const rowGap = Math.max(0, Number.parseFloat(style.rowGap) || 0)
66+
const measurement = `${nextBaseline}:${height}:${columns}:${rowGap}:${maximumCardHeight}`
67+
if (measurement === lastMeasurement) return
68+
lastMeasurement = measurement
69+
70+
const rows = Math.max(1, Math.floor((height + rowGap) / (maximumCardHeight + rowGap)))
71+
const capacity = Math.min(MAX_PAGE_SIZE, Math.max(1, columns * rows))
72+
if (capacity === lastCapacity) return
73+
lastCapacity = capacity
74+
onCapacityChange(capacity)
75+
}
76+
77+
function scheduleMeasurement(): void {
78+
cancelMeasurement()
79+
if (disposed || !enabled.value) return
80+
timer = window.setTimeout(measure, MEASUREMENT_DELAY_MS)
81+
}
82+
83+
const observer = new ResizeObserver(scheduleMeasurement)
84+
watch([viewport, grid, enabled], ([viewportElement, gridElement, isEnabled]) => {
85+
observer.disconnect()
86+
cancelMeasurement()
87+
if (!isEnabled) return
88+
if (viewportElement) observer.observe(viewportElement)
89+
if (gridElement) observer.observe(gridElement)
90+
scheduleMeasurement()
91+
}, { immediate: true, flush: 'post' })
92+
93+
window.addEventListener('resize', scheduleMeasurement)
94+
onScopeDispose(() => {
95+
disposed = true
96+
observer.disconnect()
97+
cancelMeasurement()
98+
window.removeEventListener('resize', scheduleMeasurement)
99+
})
100+
}

frontend/src/components/common/Pagination.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
>
3737
<!-- Desktop pagination info -->
3838
<div
39+
class="pagination-info"
3940
:class="[
4041
'flex items-center',
4142
compact ? 'w-full min-w-0 justify-between gap-3' : 'space-x-4'

frontend/src/components/layout/AppLayout.vue

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
<template>
2-
<div class="app-shell">
2+
<div class="app-shell" :class="{ 'app-shell-viewport': contentLayout === 'viewport' }">
33
<!-- Sidebar -->
44
<AppSidebar />
55

66
<!-- Main Content Area -->
77
<div
88
class="app-main-shell"
9-
:class="[sidebarCollapsed ? 'lg:ml-[72px]' : 'lg:ml-64']"
9+
:class="[
10+
sidebarCollapsed ? 'lg:ml-[72px]' : 'lg:ml-64',
11+
{ 'app-main-shell-viewport': contentLayout === 'viewport' },
12+
]"
1013
>
1114
<!-- Header -->
12-
<AppHeader />
15+
<AppHeader :class="{ 'app-header-viewport': contentLayout === 'viewport' }" />
1316

1417
<!-- Main Content -->
15-
<main class="app-content" :data-ui-skin="uiSkin">
18+
<main
19+
class="app-content"
20+
:class="{ 'app-content-viewport': contentLayout === 'viewport' }"
21+
:data-ui-skin="uiSkin"
22+
>
1623
<slot />
1724
</main>
1825
</div>
@@ -30,6 +37,12 @@ import AppSidebar from './AppSidebar.vue'
3037
import AppHeader from './AppHeader.vue'
3138
import { useUiSkin } from '@/composables/useUiSkin'
3239
40+
withDefaults(defineProps<{
41+
contentLayout?: 'default' | 'viewport'
42+
}>(), {
43+
contentLayout: 'default'
44+
})
45+
3346
const appStore = useAppStore()
3447
const authStore = useAuthStore()
3548
const sidebarCollapsed = computed(() => appStore.sidebarCollapsed)
@@ -49,3 +62,39 @@ onMounted(() => {
4962
5063
defineExpose({ replayTour })
5164
</script>
65+
66+
<style scoped>
67+
.app-shell.app-shell-viewport {
68+
height: 100dvh;
69+
min-height: 0;
70+
overflow: hidden;
71+
}
72+
73+
.app-main-shell.app-main-shell-viewport {
74+
display: flex;
75+
height: 100%;
76+
min-height: 0;
77+
flex-direction: column;
78+
overflow: hidden;
79+
}
80+
81+
.app-header-viewport {
82+
flex-shrink: 0;
83+
}
84+
85+
.app-content.app-content-viewport {
86+
width: 100%;
87+
min-height: 0;
88+
max-width: none;
89+
flex: 1;
90+
overflow: hidden;
91+
margin: 0;
92+
padding: 0.75rem;
93+
}
94+
95+
@media (min-width: 1024px) {
96+
.app-content.app-content-viewport {
97+
padding: 1rem;
98+
}
99+
}
100+
</style>

0 commit comments

Comments
 (0)