mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-07-15 09:23:21 +08:00
Fix dashboard media grid layout persistence and capacity
This commit is contained in:
151
src/composables/useDashboardMediaGridCapacity.ts
Normal file
151
src/composables/useDashboardMediaGridCapacity.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { computed, nextTick, onActivated, onMounted, onUnmounted, ref, watch, type ComputedRef, type Ref } from 'vue'
|
||||
|
||||
interface DashboardMediaGridCapacityOptions {
|
||||
contentSelector?: string
|
||||
gap?: number
|
||||
horizontalPadding?: number
|
||||
maxCount?: number
|
||||
minItemWidth: number
|
||||
rows?: number
|
||||
}
|
||||
|
||||
interface DashboardMediaGridCapacityState {
|
||||
columnCount: ComputedRef<number>
|
||||
containerRef: Ref<HTMLElement | null>
|
||||
itemCount: ComputedRef<number>
|
||||
refreshCapacity: () => void
|
||||
}
|
||||
|
||||
const DEFAULT_DASHBOARD_MEDIA_GRID_GAP = 16
|
||||
const DEFAULT_DASHBOARD_MEDIA_GRID_ROWS = 2
|
||||
|
||||
/**
|
||||
* 根据仪表盘媒体卡片容器宽度计算可铺满指定行数的请求数量。
|
||||
*
|
||||
* @param options 网格尺寸参数,需要与实际 ProgressiveCardGrid 参数保持一致
|
||||
* @returns 容器引用、列数、请求数量和手动刷新方法
|
||||
*/
|
||||
export function useDashboardMediaGridCapacity(options: DashboardMediaGridCapacityOptions): DashboardMediaGridCapacityState {
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const measuredWidth = ref(0)
|
||||
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
let frameId: number | null = null
|
||||
|
||||
const safeGap = computed(() => Math.max(0, options.gap ?? DEFAULT_DASHBOARD_MEDIA_GRID_GAP))
|
||||
const safeHorizontalPadding = computed(() => Math.max(0, options.horizontalPadding ?? 0))
|
||||
const safeMaxCount = computed(() => Math.max(0, Math.floor(options.maxCount ?? Number.POSITIVE_INFINITY)))
|
||||
const safeMinItemWidth = computed(() => Math.max(1, options.minItemWidth))
|
||||
const safeRows = computed(() => Math.max(1, Math.floor(options.rows ?? DEFAULT_DASHBOARD_MEDIA_GRID_ROWS)))
|
||||
|
||||
const columnCount = computed(() => {
|
||||
if (measuredWidth.value <= 0) return 0
|
||||
|
||||
return Math.max(1, Math.floor((measuredWidth.value + safeGap.value) / (safeMinItemWidth.value + safeGap.value)))
|
||||
})
|
||||
|
||||
const itemCount = computed(() => {
|
||||
if (columnCount.value <= 0) return 0
|
||||
|
||||
const count = columnCount.value * safeRows.value
|
||||
|
||||
return safeMaxCount.value > 0 ? Math.min(count, safeMaxCount.value) : count
|
||||
})
|
||||
|
||||
/**
|
||||
* 读取元素扣除内边距后的内容宽度。
|
||||
*
|
||||
* @param element 需要测量的元素
|
||||
*/
|
||||
function measureContentBoxWidth(element: HTMLElement) {
|
||||
const style = window.getComputedStyle(element)
|
||||
const paddingLeft = Number.parseFloat(style.paddingLeft) || 0
|
||||
const paddingRight = Number.parseFloat(style.paddingRight) || 0
|
||||
|
||||
return Math.max(0, element.clientWidth - paddingLeft - paddingRight)
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取容器可用于网格布局的实际宽度。
|
||||
*/
|
||||
function measureContainerWidth() {
|
||||
const element = containerRef.value
|
||||
|
||||
if (!element) return 0
|
||||
|
||||
const contentElement = options.contentSelector
|
||||
? element.querySelector<HTMLElement>(options.contentSelector)
|
||||
: null
|
||||
if (contentElement) {
|
||||
return measureContentBoxWidth(contentElement)
|
||||
}
|
||||
|
||||
const elementWidth = element.getBoundingClientRect().width || element.clientWidth
|
||||
|
||||
return Math.max(0, elementWidth - safeHorizontalPadding.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步当前容器宽度到响应式状态。
|
||||
*/
|
||||
function refreshCapacity() {
|
||||
measuredWidth.value = measureContainerWidth()
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并同一帧内的尺寸变更,避免拖拽调整仪表盘时重复计算。
|
||||
*/
|
||||
function queueRefreshCapacity() {
|
||||
if (typeof window === 'undefined' || frameId !== null) return
|
||||
|
||||
frameId = window.requestAnimationFrame(() => {
|
||||
frameId = null
|
||||
refreshCapacity()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新绑定当前容器的 ResizeObserver。
|
||||
*/
|
||||
function observeContainer() {
|
||||
resizeObserver?.disconnect()
|
||||
resizeObserver = null
|
||||
|
||||
if (typeof ResizeObserver !== 'undefined' && containerRef.value) {
|
||||
resizeObserver = new ResizeObserver(queueRefreshCapacity)
|
||||
resizeObserver.observe(containerRef.value)
|
||||
}
|
||||
|
||||
queueRefreshCapacity()
|
||||
}
|
||||
|
||||
watch(containerRef, observeContainer, { flush: 'post' })
|
||||
|
||||
onMounted(() => {
|
||||
observeContainer()
|
||||
window.addEventListener('resize', queueRefreshCapacity, { passive: true })
|
||||
void nextTick(refreshCapacity)
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
void nextTick(refreshCapacity)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', queueRefreshCapacity)
|
||||
resizeObserver?.disconnect()
|
||||
resizeObserver = null
|
||||
|
||||
if (frameId !== null) {
|
||||
window.cancelAnimationFrame(frameId)
|
||||
frameId = null
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
columnCount,
|
||||
containerRef,
|
||||
itemCount,
|
||||
refreshCapacity,
|
||||
}
|
||||
}
|
||||
@@ -111,6 +111,9 @@ const loadedDashboardGridItemIds = ref<Set<string>>(new Set())
|
||||
// 是否正在由 Vue 同步 GridStack,避免初始化写入覆盖用户布局
|
||||
const isSyncingDashboardGrid = ref(false)
|
||||
|
||||
// 是否正在把 GridStack 当前布局写回 Vue 状态,避免同源变更再次反向同步到 GridStack。
|
||||
const isPersistingDashboardGridLayoutFromGrid = ref(false)
|
||||
|
||||
// 仪表板本地布局覆盖配置
|
||||
const dashboardGridLayout = ref<DashboardGridLayoutConfig>({})
|
||||
|
||||
@@ -757,19 +760,18 @@ function updateDashboardSettingsDialog() {
|
||||
}
|
||||
|
||||
// 退出仪表板布局编辑模式;如果刚恢复默认布局,则跳过本次本地持久化。
|
||||
function exitDashboardLayoutEditing() {
|
||||
async function exitDashboardLayoutEditing() {
|
||||
if (isDashboardGridLayoutResetPending.value) {
|
||||
isDashboardGridLayoutResetPending.value = false
|
||||
} else {
|
||||
compactAndPersistDashboardGrid()
|
||||
await persistCurrentDashboardGridLayout()
|
||||
}
|
||||
|
||||
isLayoutEditing.value = false
|
||||
nextTick(() => {
|
||||
syncDashboardFillContentState()
|
||||
resizeAutoDashboardItemsToContent()
|
||||
notifyDashboardContentResize()
|
||||
})
|
||||
await nextTick()
|
||||
syncDashboardFillContentState()
|
||||
resizeAutoDashboardItemsToContent()
|
||||
notifyDashboardContentResize()
|
||||
}
|
||||
|
||||
// 清除用户本地布局覆盖,并恢复内置组件和插件声明的默认占位,然后退出编辑模式。
|
||||
@@ -780,7 +782,7 @@ async function resetDashboardGridLayout() {
|
||||
isDashboardGridLayoutResetPending.value = true
|
||||
await syncDashboardGrid()
|
||||
if (isLayoutEditing.value) {
|
||||
exitDashboardLayoutEditing()
|
||||
await exitDashboardLayoutEditing()
|
||||
} else {
|
||||
await nextTick()
|
||||
syncDashboardFillContentState()
|
||||
@@ -831,10 +833,10 @@ useDynamicButton({
|
||||
show: computed(() => appMode.value && route.path === '/dashboard'),
|
||||
})
|
||||
|
||||
// 切换仪表板布局编辑模式,退出编辑时压实并保存当前布局。
|
||||
// 切换仪表板布局编辑模式,退出编辑时保存当前布局。
|
||||
function toggleDashboardLayoutEditing() {
|
||||
if (isLayoutEditing.value) {
|
||||
exitDashboardLayoutEditing()
|
||||
void exitDashboardLayoutEditing()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1225,7 +1227,7 @@ function handleDashboardGridResize() {
|
||||
|
||||
// 保存用户拖动后的位置,并保持未手动调高组件继续按内容自适应。
|
||||
function handleDashboardGridDragStop() {
|
||||
compactAndPersistDashboardGrid(false)
|
||||
void persistCurrentDashboardGridLayout(false)
|
||||
}
|
||||
|
||||
// 保存用户缩放后的布局,只有高度发生变化时才把高度标记为手动固定。
|
||||
@@ -1238,7 +1240,7 @@ function handleDashboardGridResizeStop(_event: Event, element: GridItemHTMLEleme
|
||||
dashboardGridResizeStartHeights.delete(id)
|
||||
isDashboardGridResizing.value = false
|
||||
notifyDashboardContentResize()
|
||||
compactAndPersistDashboardGrid(heightChanged ? id : false)
|
||||
void persistCurrentDashboardGridLayout(heightChanged ? id : false)
|
||||
}
|
||||
|
||||
// 合并连续 resize 通知,模拟浏览器窗口变化让组件内部内容自适配新尺寸。
|
||||
@@ -1279,9 +1281,13 @@ function persistDashboardGridLayout(manualHeightId: string | false = false) {
|
||||
nextLayout[id] = nextItemLayout
|
||||
})
|
||||
|
||||
isPersistingDashboardGridLayoutFromGrid.value = true
|
||||
dashboardGridLayout.value = nextLayout
|
||||
saveDashboardGridLayout(nextLayout)
|
||||
nextTick(resizeAutoDashboardItemsToContent)
|
||||
nextTick(() => {
|
||||
isPersistingDashboardGridLayoutFromGrid.value = false
|
||||
resizeAutoDashboardItemsToContent()
|
||||
})
|
||||
}
|
||||
|
||||
// 根据组件 ID 查找默认宽度,保存布局时用于兜底。
|
||||
@@ -1291,13 +1297,13 @@ function getDefaultDashboardGridWidthById(id: string, maxColumns = DASHBOARD_GRI
|
||||
return item ? Math.min(getDefaultDashboardGridWidth(item), maxColumns) : maxColumns
|
||||
}
|
||||
|
||||
// 压实 GridStack 布局并保存本地占位信息。
|
||||
function compactAndPersistDashboardGrid(manualHeightId: string | false = false) {
|
||||
// 等待 GridStack 当前拖拽/缩放事件收尾后,保存用户当前看到的布局。
|
||||
async function persistCurrentDashboardGridLayout(manualHeightId: string | false = false) {
|
||||
if (!dashboardGrid.value || isSyncingDashboardGrid.value) return
|
||||
|
||||
isDashboardGridLayoutResetPending.value = false
|
||||
dashboardGrid.value.compact('compact')
|
||||
nextTick(() => persistDashboardGridLayout(manualHeightId))
|
||||
await nextTick()
|
||||
persistDashboardGridLayout(manualHeightId)
|
||||
}
|
||||
|
||||
watch(isLayoutEditing, value => {
|
||||
@@ -1308,7 +1314,9 @@ watch(
|
||||
dashboardGridItems,
|
||||
() => {
|
||||
syncDashboardLoadedItemIds()
|
||||
syncDashboardGrid()
|
||||
if (!isPersistingDashboardGridLayoutFromGrid.value) {
|
||||
syncDashboardGrid()
|
||||
}
|
||||
scheduleDashboardReveal()
|
||||
},
|
||||
{ deep: true },
|
||||
|
||||
@@ -3,17 +3,34 @@ import api from '@/api'
|
||||
import type { MediaServerConf, MediaServerPlayItem } from '@/api/types'
|
||||
import PosterCard from '@/components/cards/PosterCard.vue'
|
||||
import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
|
||||
import { useDashboardMediaGridCapacity } from '@/composables/useDashboardMediaGridCapacity'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
const LATEST_CARD_MIN_WIDTH = 144
|
||||
const MEDIA_GRID_HORIZONTAL_PADDING = 40
|
||||
|
||||
// 最近入库列表
|
||||
const latestList = ref<{ [key: string]: MediaServerPlayItem[] }>({})
|
||||
|
||||
// 所有媒体服务器设置
|
||||
const mediaServers = ref<MediaServerConf[]>([])
|
||||
|
||||
// 最近入库两行网格容量
|
||||
const {
|
||||
containerRef: mediaGridContainerRef,
|
||||
itemCount: latestItemCount,
|
||||
refreshCapacity,
|
||||
} = useDashboardMediaGridCapacity({
|
||||
contentSelector: '.dashboard-media-content',
|
||||
horizontalPadding: MEDIA_GRID_HORIZONTAL_PADDING,
|
||||
minItemWidth: LATEST_CARD_MIN_WIDTH,
|
||||
})
|
||||
|
||||
let latestLoadId = 0
|
||||
|
||||
/**
|
||||
* 查询媒体服务器设置。
|
||||
*/
|
||||
@@ -29,16 +46,17 @@ async function loadMediaServerSetting() {
|
||||
/**
|
||||
* 查询指定媒体服务器的最近入库列表。
|
||||
* @param server 媒体服务器名称
|
||||
* @param count 需要返回的条目数量
|
||||
*/
|
||||
async function loadLatest(server: string) {
|
||||
async function loadLatest(server: string, count: number) {
|
||||
try {
|
||||
const response: MediaServerPlayItem[] = await api.get('mediaserver/latest', { params: { server } })
|
||||
// 仅在有数据时赋值
|
||||
if (response && response.length > 0) {
|
||||
latestList.value[server] = response
|
||||
}
|
||||
const response: MediaServerPlayItem[] = await api.get('mediaserver/latest', { params: { count, server } })
|
||||
|
||||
return response ?? []
|
||||
} catch (e) {
|
||||
console.log(t('dashboard.errors.loadLatest', { server }), e)
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,24 +64,48 @@ async function loadLatest(server: string) {
|
||||
* 加载已启用媒体服务器的最近入库数据。
|
||||
*/
|
||||
async function loadData() {
|
||||
const count = latestItemCount.value
|
||||
if (count <= 0) return
|
||||
|
||||
const loadId = ++latestLoadId
|
||||
|
||||
await loadMediaServerSetting()
|
||||
if (loadId !== latestLoadId) return
|
||||
|
||||
const enabledServers = mediaServers.value.filter(server => server.enabled)
|
||||
for (const server of enabledServers) {
|
||||
loadLatest(server.name)
|
||||
}
|
||||
const entries = await Promise.all(
|
||||
enabledServers.map(async server => [server.name, await loadLatest(server.name, count)] as const),
|
||||
)
|
||||
|
||||
if (loadId !== latestLoadId) return
|
||||
|
||||
latestList.value = entries.reduce<{ [key: string]: MediaServerPlayItem[] }>((result, [name, data]) => {
|
||||
if (data.length > 0) {
|
||||
result[name] = data.slice(0, count)
|
||||
}
|
||||
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
watch(latestItemCount, count => {
|
||||
if (count <= 0) return
|
||||
|
||||
loadData()
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
refreshCapacity()
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dashboard-media-stack" :class="{ 'dashboard-grid-fill': Object.keys(latestList).length > 0 }">
|
||||
<div
|
||||
ref="mediaGridContainerRef"
|
||||
class="dashboard-media-stack"
|
||||
:class="{ 'dashboard-grid-fill': Object.keys(latestList).length > 0 }"
|
||||
>
|
||||
<VCard v-for="(data, name) in latestList" :key="name" class="dashboard-work-card dashboard-media-card">
|
||||
<VCardItem class="dashboard-media-header">
|
||||
<VCardTitle>{{ t('dashboard.latest') }} - {{ name }}</VCardTitle>
|
||||
@@ -72,9 +114,9 @@ onActivated(() => {
|
||||
<div class="dashboard-media-content px-5 pb-3">
|
||||
<ProgressiveCardGrid
|
||||
class="dashboard-media-grid"
|
||||
:items="data"
|
||||
:items="data.slice(0, latestItemCount)"
|
||||
:get-item-key="item => item.id || item.link || item.title"
|
||||
:min-item-width="144"
|
||||
:min-item-width="LATEST_CARD_MIN_WIDTH"
|
||||
:item-aspect-ratio="1.5"
|
||||
tabindex="0"
|
||||
>
|
||||
|
||||
@@ -3,17 +3,36 @@ import api from '@/api'
|
||||
import type { MediaServerConf, MediaServerPlayItem } from '@/api/types'
|
||||
import PlayingBackdropCard from '@/components/cards/PlayingBackdropCard.vue'
|
||||
import ProgressiveCardGrid from '@/components/misc/ProgressiveCardGrid.vue'
|
||||
import { useDashboardMediaGridCapacity } from '@/composables/useDashboardMediaGridCapacity'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
const PLAYING_CARD_MIN_WIDTH = 240
|
||||
const MEDIA_GRID_HORIZONTAL_PADDING = 40
|
||||
|
||||
// 继续播放列表
|
||||
const playingList = ref<MediaServerPlayItem[]>([])
|
||||
|
||||
// 所有媒体服务器设置
|
||||
const mediaServers = ref<MediaServerConf[]>([])
|
||||
|
||||
// 继续观看两行网格容量
|
||||
const {
|
||||
containerRef: mediaGridContainerRef,
|
||||
itemCount: playingItemCount,
|
||||
refreshCapacity,
|
||||
} = useDashboardMediaGridCapacity({
|
||||
contentSelector: '.dashboard-media-content',
|
||||
horizontalPadding: MEDIA_GRID_HORIZONTAL_PADDING,
|
||||
minItemWidth: PLAYING_CARD_MIN_WIDTH,
|
||||
})
|
||||
|
||||
const displayedPlayingList = computed(() => playingList.value.slice(0, playingItemCount.value))
|
||||
|
||||
let playingLoadId = 0
|
||||
|
||||
/**
|
||||
* 查询媒体服务器设置。
|
||||
*/
|
||||
@@ -29,21 +48,17 @@ async function loadMediaServerSetting() {
|
||||
/**
|
||||
* 查询指定媒体服务器的继续观看列表。
|
||||
* @param server 媒体服务器名称
|
||||
* @param count 需要返回的条目数量
|
||||
*/
|
||||
async function loadPlayingList(server: string) {
|
||||
async function loadPlayingList(server: string, count: number) {
|
||||
try {
|
||||
const result: MediaServerPlayItem[] = await api.get('mediaserver/playing', { params: { server } })
|
||||
if (result && result.length > 0) {
|
||||
// 不存在时添加
|
||||
for (const item of result) {
|
||||
const index = playingList.value.findIndex(i => i.id === item.id)
|
||||
if (index === -1) {
|
||||
playingList.value.push(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
const result: MediaServerPlayItem[] = await api.get('mediaserver/playing', { params: { count, server } })
|
||||
|
||||
return result ?? []
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,43 +66,70 @@ async function loadPlayingList(server: string) {
|
||||
* 加载已启用媒体服务器的继续观看数据。
|
||||
*/
|
||||
async function loadData() {
|
||||
const count = playingItemCount.value
|
||||
if (count <= 0) return
|
||||
|
||||
const loadId = ++playingLoadId
|
||||
|
||||
await loadMediaServerSetting()
|
||||
if (loadId !== playingLoadId) return
|
||||
|
||||
const enabledServers = mediaServers.value.filter(server => server.enabled)
|
||||
for (const server of enabledServers) {
|
||||
loadPlayingList(server.name)
|
||||
}
|
||||
const serverItems = await Promise.all(enabledServers.map(server => loadPlayingList(server.name, count)))
|
||||
|
||||
if (loadId !== playingLoadId) return
|
||||
|
||||
const itemMap = new Map<string, MediaServerPlayItem>()
|
||||
|
||||
serverItems.flat().forEach((item, index) => {
|
||||
const key = String(item.id || item.link || `${item.server_type || 'server'}-${item.title}-${index}`)
|
||||
if (!itemMap.has(key)) {
|
||||
itemMap.set(key, item)
|
||||
}
|
||||
})
|
||||
|
||||
playingList.value = Array.from(itemMap.values()).slice(0, count)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
watch(playingItemCount, count => {
|
||||
if (count <= 0) return
|
||||
|
||||
loadData()
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
refreshCapacity()
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard v-if="playingList.length > 0" class="dashboard-media-card dashboard-grid-fill">
|
||||
<VCardItem class="dashboard-media-header">
|
||||
<VCardTitle>{{ t('dashboard.playing') }}</VCardTitle>
|
||||
</VCardItem>
|
||||
<div
|
||||
ref="mediaGridContainerRef"
|
||||
class="dashboard-media-shell"
|
||||
:class="{ 'dashboard-grid-fill': displayedPlayingList.length > 0 }"
|
||||
>
|
||||
<VCard v-if="displayedPlayingList.length > 0" class="dashboard-media-card">
|
||||
<VCardItem class="dashboard-media-header">
|
||||
<VCardTitle>{{ t('dashboard.playing') }}</VCardTitle>
|
||||
</VCardItem>
|
||||
|
||||
<div class="dashboard-media-content px-5 pb-3">
|
||||
<ProgressiveCardGrid
|
||||
class="dashboard-media-grid"
|
||||
:items="playingList"
|
||||
:get-item-key="item => item.id || item.link || item.title"
|
||||
:min-item-width="240"
|
||||
:estimated-item-height="174"
|
||||
tabindex="0"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<PlayingBackdropCard :media="item" height="10.875rem" />
|
||||
</template>
|
||||
</ProgressiveCardGrid>
|
||||
</div>
|
||||
</VCard>
|
||||
<div class="dashboard-media-content px-5 pb-3">
|
||||
<ProgressiveCardGrid
|
||||
class="dashboard-media-grid"
|
||||
:items="displayedPlayingList"
|
||||
:get-item-key="item => item.id || item.link || item.title"
|
||||
:min-item-width="PLAYING_CARD_MIN_WIDTH"
|
||||
:estimated-item-height="174"
|
||||
tabindex="0"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<PlayingBackdropCard :media="item" height="10.875rem" />
|
||||
</template>
|
||||
</ProgressiveCardGrid>
|
||||
</div>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -98,6 +140,11 @@ onActivated(() => {
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.dashboard-media-shell {
|
||||
block-size: 100%;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.dashboard-media-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user