mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-16 13:11:22 +08:00
feat(dashboard): enhance layout and responsiveness of dashboard components
This commit is contained in:
@@ -71,6 +71,7 @@ const dashboardGridPendingContentResize = new Set<GridItemHTMLElement>()
|
||||
|
||||
let dashboardGridContentObserver: ResizeObserver | null = null
|
||||
let dashboardGridContentResizeFrame: number | null = null
|
||||
let dashboardGridResizeRefreshFrame: number | null = null
|
||||
|
||||
// 是否正在手动缩放组件,避免自动测高抢回用户拖动中的高度。
|
||||
const isDashboardGridResizing = ref(false)
|
||||
@@ -590,6 +591,7 @@ function initializeDashboardGrid() {
|
||||
|
||||
dashboardGrid.value.on('dragstop', handleDashboardGridDragStop)
|
||||
dashboardGrid.value.on('resizestart', handleDashboardGridResizeStart)
|
||||
dashboardGrid.value.on('resize', handleDashboardGridResize)
|
||||
dashboardGrid.value.on('resizestop', handleDashboardGridResizeStop)
|
||||
updateDashboardGridEditableState(isLayoutEditing.value)
|
||||
syncDashboardGrid()
|
||||
@@ -724,6 +726,12 @@ function handleDashboardGridResizeStart(_event: Event, element: GridItemHTMLElem
|
||||
|
||||
isDashboardGridResizing.value = true
|
||||
dashboardGridResizeStartHeights.set(id, element.gridstackNode?.h)
|
||||
notifyDashboardContentResize()
|
||||
}
|
||||
|
||||
// 在用户缩放过程中通知图表、虚拟网格等内容重新读取容器尺寸。
|
||||
function handleDashboardGridResize() {
|
||||
notifyDashboardContentResize()
|
||||
}
|
||||
|
||||
// 保存用户拖动后的位置,并保持未手动调高组件继续按内容自适应。
|
||||
@@ -740,9 +748,20 @@ function handleDashboardGridResizeStop(_event: Event, element: GridItemHTMLEleme
|
||||
|
||||
dashboardGridResizeStartHeights.delete(id)
|
||||
isDashboardGridResizing.value = false
|
||||
notifyDashboardContentResize()
|
||||
compactAndPersistDashboardGrid(heightChanged ? id : false)
|
||||
}
|
||||
|
||||
// 合并连续 resize 通知,模拟浏览器窗口变化让组件内部内容自适配新尺寸。
|
||||
function notifyDashboardContentResize() {
|
||||
if (typeof window === 'undefined' || dashboardGridResizeRefreshFrame !== null) return
|
||||
|
||||
dashboardGridResizeRefreshFrame = window.requestAnimationFrame(() => {
|
||||
dashboardGridResizeRefreshFrame = null
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
})
|
||||
}
|
||||
|
||||
// 将 GridStack 保存结果归一化为本地布局覆盖表。
|
||||
function persistDashboardGridLayout(manualHeightId: string | false = false) {
|
||||
if (!dashboardGrid.value || isSyncingDashboardGrid.value) return
|
||||
@@ -831,6 +850,10 @@ onBeforeUnmount(() => {
|
||||
cancelAnimationFrame(dashboardGridContentResizeFrame)
|
||||
dashboardGridContentResizeFrame = null
|
||||
}
|
||||
if (dashboardGridResizeRefreshFrame !== null) {
|
||||
cancelAnimationFrame(dashboardGridResizeRefreshFrame)
|
||||
dashboardGridResizeRefreshFrame = null
|
||||
}
|
||||
dashboardGridPendingContentResize.clear()
|
||||
dashboardGridResizeStartHeights.clear()
|
||||
dashboardGrid.value?.destroy(false)
|
||||
@@ -900,6 +923,17 @@ onBeforeUnmount(() => {
|
||||
margin-block: -6px 0;
|
||||
}
|
||||
|
||||
.dashboard-grid :deep(.v-card) {
|
||||
overflow: hidden;
|
||||
box-shadow: var(--app-surface-shadow) !important;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.dashboard-grid :deep(.v-card:hover) {
|
||||
box-shadow: var(--app-surface-hover-shadow) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-grid-item.is-manual-height :deep(.v-card) {
|
||||
block-size: 100%;
|
||||
}
|
||||
@@ -928,6 +962,39 @@ onBeforeUnmount(() => {
|
||||
block-size: 100%;
|
||||
}
|
||||
|
||||
.dashboard-grid :deep(.dashboard-chart-card),
|
||||
.dashboard-grid :deep(.dashboard-summary-card),
|
||||
.dashboard-grid :deep(.dashboard-work-card),
|
||||
.dashboard-grid :deep(.dashboard-media-card) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.dashboard-grid :deep(.dashboard-chart-card .v-card-text),
|
||||
.dashboard-grid :deep(.dashboard-work-card .v-card-text),
|
||||
.dashboard-grid :deep(.dashboard-card-grid-wrap) {
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.dashboard-grid:not(.is-editing) .dashboard-grid-item:not(.is-manual-height) :deep(.dashboard-summary-card) {
|
||||
min-block-size: 160px;
|
||||
}
|
||||
|
||||
.dashboard-grid:not(.is-editing) .dashboard-grid-item:not(.is-manual-height) :deep(.dashboard-chart-card) {
|
||||
min-block-size: 256px;
|
||||
}
|
||||
|
||||
.dashboard-grid:not(.is-editing) .dashboard-grid-item:not(.is-manual-height) :deep(.dashboard-work-card) {
|
||||
min-block-size: 352px;
|
||||
}
|
||||
|
||||
.dashboard-grid.is-editing :deep(.v-card-text),
|
||||
.dashboard-grid-item.is-manual-height :deep(.v-card-text) {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.dashboard-grid-drag-handle {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
|
||||
@@ -139,8 +139,10 @@ useKeepAliveRefresh(refresh)
|
||||
<VCardItem>
|
||||
<VCardTitle>CPU</VCardTitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VApexChart type="line" :options="chartOptions" :series="series" :height="150" />
|
||||
<VCardText class="dashboard-chart-content">
|
||||
<div class="dashboard-chart-plot">
|
||||
<VApexChart type="line" :options="chartOptions" :series="series" height="100%" />
|
||||
</div>
|
||||
<p class="text-center font-weight-medium mb-0">{{ t('dashboard.current') }}:{{ current }}%</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
@@ -149,7 +151,21 @@ useKeepAliveRefresh(refresh)
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-chart-card {
|
||||
min-block-size: 256px;
|
||||
.dashboard-chart-content {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.dashboard-chart-plot {
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.dashboard-chart-plot :deep(.vue-apexcharts),
|
||||
.dashboard-chart-plot :deep(.apexcharts-canvas),
|
||||
.dashboard-chart-plot :deep(svg) {
|
||||
block-size: 100% !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -85,9 +85,3 @@ onActivated(() => {
|
||||
</template>
|
||||
</VHover>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-summary-card {
|
||||
min-block-size: 160px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -144,8 +144,10 @@ useKeepAliveRefresh(refresh)
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ t('dashboard.memory') }}</VCardTitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VApexChart type="area" :options="chartOptions" :series="series" :height="150" />
|
||||
<VCardText class="dashboard-chart-content">
|
||||
<div class="dashboard-chart-plot">
|
||||
<VApexChart type="area" :options="chartOptions" :series="series" height="100%" />
|
||||
</div>
|
||||
<p class="text-center font-weight-medium mb-0">{{ t('dashboard.current') }}:{{ formatBytes(usedMemory) }}</p>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
@@ -154,7 +156,21 @@ useKeepAliveRefresh(refresh)
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-chart-card {
|
||||
min-block-size: 256px;
|
||||
.dashboard-chart-content {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.dashboard-chart-plot {
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.dashboard-chart-plot :deep(.vue-apexcharts),
|
||||
.dashboard-chart-plot :deep(.apexcharts-canvas),
|
||||
.dashboard-chart-plot :deep(svg) {
|
||||
block-size: 100% !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -177,8 +177,10 @@ useKeepAliveRefresh(refresh)
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ t('dashboard.network') }}</VCardTitle>
|
||||
</VCardItem>
|
||||
<VCardText>
|
||||
<VApexChart type="line" :options="chartOptions" :series="series" :height="150" />
|
||||
<VCardText class="dashboard-chart-content">
|
||||
<div class="dashboard-chart-plot">
|
||||
<VApexChart type="line" :options="chartOptions" :series="series" height="100%" />
|
||||
</div>
|
||||
<div class="d-flex justify-space-between">
|
||||
<p class="text-center font-weight-medium mb-0">
|
||||
<span class="text-warning">{{ t('dashboard.upload') }}</span
|
||||
@@ -196,7 +198,21 @@ useKeepAliveRefresh(refresh)
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-chart-card {
|
||||
min-block-size: 256px;
|
||||
.dashboard-chart-content {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.dashboard-chart-plot {
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.dashboard-chart-plot :deep(.vue-apexcharts),
|
||||
.dashboard-chart-plot :deep(.apexcharts-canvas),
|
||||
.dashboard-chart-plot :deep(svg) {
|
||||
block-size: 100% !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -51,8 +51,8 @@ useDataRefresh(
|
||||
<VCardTitle>{{ t('dashboard.scheduler') }}</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<VList class="card-list" height="260">
|
||||
<VCardText class="dashboard-work-content">
|
||||
<VList class="card-list">
|
||||
<VListItem v-for="item in schedulerList" :key="item.id">
|
||||
<template #prepend>
|
||||
<VAvatar size="40" variant="tonal" color="" class="me-3">
|
||||
@@ -89,10 +89,17 @@ useDataRefresh(
|
||||
<style lang="scss" scoped>
|
||||
.card-list {
|
||||
--v-card-list-gap: 1.5rem;
|
||||
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.dashboard-work-card {
|
||||
min-block-size: 352px;
|
||||
.dashboard-work-content {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.card-list::-webkit-scrollbar {
|
||||
|
||||
@@ -94,7 +94,7 @@ const { loading } = useDataRefresh(
|
||||
<VCardTitle>{{ t('dashboard.realTimeSpeed') }}</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText class="pt-4">
|
||||
<VCardText class="dashboard-work-content pt-4">
|
||||
<div>
|
||||
<p class="text-h5 me-2">↑{{ formatFileSize(downloadInfo.upload_speed) }}/s</p>
|
||||
<p class="text-h4 me-2">↓{{ formatFileSize(downloadInfo.download_speed) }}/s</p>
|
||||
@@ -127,9 +127,16 @@ const { loading } = useDataRefresh(
|
||||
<style lang="scss" scoped>
|
||||
.card-list {
|
||||
--v-card-list-gap: 1rem;
|
||||
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.dashboard-work-card {
|
||||
min-block-size: 352px;
|
||||
.dashboard-work-content {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-block-size: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -88,7 +88,4 @@ onActivated(() => {
|
||||
inset-inline-end: 2rem;
|
||||
}
|
||||
|
||||
.dashboard-summary-card {
|
||||
min-block-size: 160px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -138,8 +138,10 @@ onActivated(() => {
|
||||
<VCardTitle>{{ t('dashboard.weeklyOverview') }}</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<VCardText>
|
||||
<VApexChart type="bar" :options="options" :series="series" :height="160" />
|
||||
<VCardText class="dashboard-work-content">
|
||||
<div class="dashboard-work-chart">
|
||||
<VApexChart type="bar" :options="options" :series="series" height="100%" />
|
||||
</div>
|
||||
<div class="d-flex align-center mb-3">
|
||||
<h5 class="text-h5 me-4">
|
||||
{{ totalCount }}
|
||||
@@ -155,7 +157,21 @@ onActivated(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-work-card {
|
||||
min-block-size: 352px;
|
||||
.dashboard-work-content {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.dashboard-work-chart {
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.dashboard-work-chart :deep(.vue-apexcharts),
|
||||
.dashboard-work-chart :deep(.apexcharts-canvas),
|
||||
.dashboard-work-chart :deep(svg) {
|
||||
block-size: 100% !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -60,7 +60,7 @@ onActivated(() => {
|
||||
<div>
|
||||
<VHover v-for="(data, name) in latestList" :key="name">
|
||||
<template #default="hover">
|
||||
<VCard v-bind="hover.props" class="dashboard-work-card">
|
||||
<VCard v-bind="hover.props" class="dashboard-work-card dashboard-media-card">
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ t('dashboard.latest') }} - {{ name }}</VCardTitle>
|
||||
</VCardItem>
|
||||
@@ -87,11 +87,9 @@ onActivated(() => {
|
||||
<style scoped>
|
||||
.dashboard-card-grid-wrap {
|
||||
/* 用内边距提供卡片留白,避免 100% 宽度网格叠加横向外边距后在 iOS 小屏溢出。 */
|
||||
min-block-size: 260px;
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
overflow: auto;
|
||||
padding: 0 0.75rem 0.75rem;
|
||||
}
|
||||
|
||||
.dashboard-work-card {
|
||||
min-block-size: 352px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -63,7 +63,7 @@ onActivated(() => {
|
||||
<template>
|
||||
<VHover v-if="libraryList.length > 0">
|
||||
<template #default="hover">
|
||||
<VCard v-bind="hover.props">
|
||||
<VCard v-bind="hover.props" class="dashboard-media-card">
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ t('dashboard.library') }}</VCardTitle>
|
||||
</VCardItem>
|
||||
@@ -88,6 +88,9 @@ onActivated(() => {
|
||||
<style scoped>
|
||||
.dashboard-card-grid-wrap {
|
||||
/* 用内边距提供卡片留白,避免 100% 宽度网格叠加横向外边距后在 iOS 小屏溢出。 */
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
overflow: auto;
|
||||
padding: 0 0.75rem 0.75rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -63,7 +63,7 @@ onActivated(() => {
|
||||
<template>
|
||||
<VHover v-if="playingList.length > 0">
|
||||
<template #default="hover">
|
||||
<VCard v-bind="hover.props">
|
||||
<VCard v-bind="hover.props" class="dashboard-media-card">
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ t('dashboard.playing') }}</VCardTitle>
|
||||
</VCardItem>
|
||||
@@ -89,6 +89,9 @@ onActivated(() => {
|
||||
<style scoped>
|
||||
.dashboard-card-grid-wrap {
|
||||
/* 用内边距提供卡片留白,避免 100% 宽度网格叠加横向外边距后在 iOS 小屏溢出。 */
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
overflow: auto;
|
||||
padding: 0 0.75rem 0.75rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user