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

@@ -16,8 +16,6 @@ const props = withDefaults(
items: UnknownRecord[] items: UnknownRecord[]
labelGetter?: (item: UnknownRecord) => string labelGetter?: (item: UnknownRecord) => string
modelValue?: boolean modelValue?: boolean
resetIcon?: string
resetText?: string
selectAllText?: string selectAllText?: string
selectNoneText?: string selectNoneText?: string
showBulkActions?: boolean showBulkActions?: boolean
@@ -30,8 +28,6 @@ const props = withDefaults(
elevated: false, elevated: false,
labelGetter: undefined, labelGetter: undefined,
modelValue: true, modelValue: true,
resetIcon: 'mdi-restore',
resetText: '',
selectAllText: '', selectAllText: '',
selectNoneText: '', selectNoneText: '',
showBulkActions: false, showBulkActions: false,
@@ -42,7 +38,6 @@ const props = withDefaults(
const emit = defineEmits<{ const emit = defineEmits<{
(event: 'close'): void (event: 'close'): void
(event: 'reset'): void
(event: 'save', payload: { elevated: boolean; enabled: Record<string, boolean> }): void (event: 'save', payload: { elevated: boolean; enabled: Record<string, boolean> }): void
(event: 'update:elevated', value: boolean): void (event: 'update:elevated', value: boolean): void
(event: 'update:modelValue', value: boolean): void (event: 'update:modelValue', value: boolean): void
@@ -104,11 +99,6 @@ function setAllItems(value: boolean) {
}) })
} }
// 触发调用方提供的重置动作。
function triggerResetAction() {
emit('reset')
}
// 提交通用内容开关设置。 // 提交通用内容开关设置。
function submitSettings() { function submitSettings() {
emit('save', { emit('save', {
@@ -157,12 +147,6 @@ function submitSettings() {
</p> </p>
</VCardText> </VCardText>
<VCardActions class="pt-3"> <VCardActions class="pt-3">
<VBtn v-if="props.resetText" variant="text" color="secondary" @click="triggerResetAction">
<template #prepend>
<VIcon :icon="props.resetIcon" />
</template>
{{ props.resetText }}
</VBtn>
<VBtn v-if="props.showBulkActions" variant="text" @click="setAllItems(true)"> <VBtn v-if="props.showBulkActions" variant="text" @click="setAllItems(true)">
{{ props.selectAllText }} {{ props.selectAllText }}
</VBtn> </VBtn>

View File

@@ -140,30 +140,28 @@ onUnmounted(() => {
<component :is="dynamicPluginComponent" :config="props.config" :allow-refresh="props.allowRefresh" :api="api" /> <component :is="dynamicPluginComponent" :config="props.config" :allow-refresh="props.allowRefresh" :api="api" />
</div> </div>
<!-- Vuetify 渲染模式 --> <!-- Vuetify 渲染模式 -->
<VHover v-else-if="pluginRenderMode === 'vuetify'"> <template v-else-if="pluginRenderMode === 'vuetify'">
<template #default="hover"> <!-- 无边框 -->
<!-- 无边框 --> <div v-if="props.config?.attrs.border === false">
<div v-if="props.config?.attrs.border === false"> <VCard>
<VCard v-bind="hover.props"> <VCardText class="p-0">
<VCardText class="p-0">
<DashboardRender v-for="(item, index) in props.config?.elements" :key="index" :config="item" />
</VCardText>
</VCard>
</div>
<!-- 有边框 -->
<VCard v-else v-bind="hover.props">
<VCardItem v-if="props.config?.attrs.border !== false">
<VCardTitle>
{{ props.config?.attrs?.title ?? props.config?.name }}
</VCardTitle>
<VCardSubtitle v-if="props.config?.attrs?.subtitle"> {{ props.config?.attrs?.subtitle }}</VCardSubtitle>
</VCardItem>
<VCardText>
<DashboardRender v-for="(item, index) in props.config?.elements" :key="index" :config="item" /> <DashboardRender v-for="(item, index) in props.config?.elements" :key="index" :config="item" />
</VCardText> </VCardText>
</VCard> </VCard>
</template> </div>
</VHover> <!-- 有边框 -->
<VCard v-else>
<VCardItem v-if="props.config?.attrs.border !== false">
<VCardTitle>
{{ props.config?.attrs?.title ?? props.config?.name }}
</VCardTitle>
<VCardSubtitle v-if="props.config?.attrs?.subtitle"> {{ props.config?.attrs?.subtitle }}</VCardSubtitle>
</VCardItem>
<VCardText>
<DashboardRender v-for="(item, index) in props.config?.elements" :key="index" :config="item" />
</VCardText>
</VCard>
</template>
<!-- 未知模式或错误 --> <!-- 未知模式或错误 -->
<VCard v-else> <VCard v-else>
<VCardText>无法渲染插件仪表盘部件: 未知渲染模式或配置错误</VCardText> <VCardText>无法渲染插件仪表盘部件: 未知渲染模式或配置错误</VCardText>

View File

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

View File

@@ -133,21 +133,17 @@ useKeepAliveRefresh(refresh)
</script> </script>
<template> <template>
<VHover> <VCard class="dashboard-chart-card">
<template #default="hover"> <VCardItem>
<VCard v-bind="hover.props" class="dashboard-chart-card"> <VCardTitle>CPU</VCardTitle>
<VCardItem> </VCardItem>
<VCardTitle>CPU</VCardTitle> <VCardText class="dashboard-chart-content">
</VCardItem> <div class="dashboard-chart-plot">
<VCardText class="dashboard-chart-content"> <VApexChart type="line" :options="chartOptions" :series="series" height="100%" />
<div class="dashboard-chart-plot"> </div>
<VApexChart type="line" :options="chartOptions" :series="series" height="100%" /> <p class="text-center font-weight-medium mb-0">{{ t('dashboard.current') }}{{ current }}%</p>
</div> </VCardText>
<p class="text-center font-weight-medium mb-0">{{ t('dashboard.current') }}{{ current }}%</p> </VCard>
</VCardText>
</VCard>
</template>
</VHover>
</template> </template>
<style scoped> <style scoped>

View File

@@ -54,34 +54,30 @@ onActivated(() => {
</script> </script>
<template> <template>
<VHover> <VCard class="dashboard-summary-card">
<template #default="hover"> <VCardItem>
<VCard v-bind="hover.props" class="dashboard-summary-card"> <VCardTitle>{{ t('dashboard.mediaStatistic') }}</VCardTitle>
<VCardItem> </VCardItem>
<VCardTitle>{{ t('dashboard.mediaStatistic') }}</VCardTitle>
</VCardItem>
<VCardText> <VCardText>
<VRow> <VRow>
<VCol v-for="item in statistics" :key="item.title" cols="6" sm="3"> <VCol v-for="item in statistics" :key="item.title" cols="6" sm="3">
<div class="d-flex align-center"> <div class="d-flex align-center">
<div class="me-3"> <div class="me-3">
<VAvatar :color="item.color" rounded size="42" class="elevation-1"> <VAvatar :color="item.color" rounded size="42" class="elevation-1">
<VIcon size="24" :icon="item.icon" /> <VIcon size="24" :icon="item.icon" />
</VAvatar> </VAvatar>
</div> </div>
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<span class="text-caption"> <span class="text-caption">
{{ item.title }} {{ item.title }}
</span> </span>
<span class="text-h6">{{ item.stats }}</span> <span class="text-h6">{{ item.stats }}</span>
</div> </div>
</div> </div>
</VCol> </VCol>
</VRow> </VRow>
</VCardText> </VCardText>
</VCard> </VCard>
</template>
</VHover>
</template> </template>

View File

@@ -138,21 +138,17 @@ useKeepAliveRefresh(refresh)
</script> </script>
<template> <template>
<VHover> <VCard class="dashboard-chart-card">
<template #default="hover"> <VCardItem>
<VCard v-bind="hover.props" class="dashboard-chart-card"> <VCardTitle>{{ t('dashboard.memory') }}</VCardTitle>
<VCardItem> </VCardItem>
<VCardTitle>{{ t('dashboard.memory') }}</VCardTitle> <VCardText class="dashboard-chart-content">
</VCardItem> <div class="dashboard-chart-plot">
<VCardText class="dashboard-chart-content"> <VApexChart type="area" :options="chartOptions" :series="series" height="100%" />
<div class="dashboard-chart-plot"> </div>
<VApexChart type="area" :options="chartOptions" :series="series" height="100%" /> <p class="text-center font-weight-medium mb-0">{{ t('dashboard.current') }}{{ formatBytes(usedMemory) }}</p>
</div> </VCardText>
<p class="text-center font-weight-medium mb-0">{{ t('dashboard.current') }}{{ formatBytes(usedMemory) }}</p> </VCard>
</VCardText>
</VCard>
</template>
</VHover>
</template> </template>
<style scoped> <style scoped>

View File

@@ -171,30 +171,26 @@ useKeepAliveRefresh(refresh)
</script> </script>
<template> <template>
<VHover> <VCard class="dashboard-chart-card">
<template #default="hover"> <VCardItem>
<VCard v-bind="hover.props" class="dashboard-chart-card"> <VCardTitle>{{ t('dashboard.network') }}</VCardTitle>
<VCardItem> </VCardItem>
<VCardTitle>{{ t('dashboard.network') }}</VCardTitle> <VCardText class="dashboard-chart-content">
</VCardItem> <div class="dashboard-chart-plot">
<VCardText class="dashboard-chart-content"> <VApexChart type="line" :options="chartOptions" :series="series" height="100%" />
<div class="dashboard-chart-plot"> </div>
<VApexChart type="line" :options="chartOptions" :series="series" height="100%" /> <div class="d-flex justify-space-between">
</div> <p class="text-center font-weight-medium mb-0">
<div class="d-flex justify-space-between"> <span class="text-warning">{{ t('dashboard.upload') }}</span
<p class="text-center font-weight-medium mb-0"> >{{ formatBytes(currentUpload) }}
<span class="text-warning">{{ t('dashboard.upload') }}</span </p>
>{{ formatBytes(currentUpload) }} <p class="text-center font-weight-medium mb-0">
</p> <span class="text-info">{{ t('dashboard.download') }}</span
<p class="text-center font-weight-medium mb-0"> >{{ formatBytes(currentDownload) }}
<span class="text-info">{{ t('dashboard.download') }}</span </p>
>{{ formatBytes(currentDownload) }} </div>
</p> </VCardText>
</div> </VCard>
</VCardText>
</VCard>
</template>
</VHover>
</template> </template>
<style scoped> <style scoped>

View File

@@ -44,46 +44,42 @@ useDataRefresh(
</script> </script>
<template> <template>
<VHover> <VCard class="dashboard-work-card">
<template #default="hover"> <VCardItem>
<VCard v-bind="hover.props" class="dashboard-work-card"> <VCardTitle>{{ t('dashboard.scheduler') }}</VCardTitle>
<VCardItem> </VCardItem>
<VCardTitle>{{ t('dashboard.scheduler') }}</VCardTitle>
</VCardItem>
<VCardText class="dashboard-work-content"> <VCardText class="dashboard-work-content">
<VList class="card-list"> <VList class="card-list">
<VListItem v-for="item in schedulerList" :key="item.id"> <VListItem v-for="item in schedulerList" :key="item.id">
<template #prepend> <template #prepend>
<VAvatar size="40" variant="tonal" color="" class="me-3"> <VAvatar size="40" variant="tonal" color="" class="me-3">
{{ item.name[0] }} {{ item.name[0] }}
</VAvatar> </VAvatar>
</template> </template>
<VListItemTitle class="mb-1"> <VListItemTitle class="mb-1">
<span class="text-sm font-weight-medium">{{ item.name }}</span> <span class="text-sm font-weight-medium">{{ item.name }}</span>
</VListItemTitle> </VListItemTitle>
<VListItemSubtitle class="text-xs"> <VListItemSubtitle class="text-xs">
{{ item.next_run }} {{ item.next_run }}
</VListItemSubtitle> </VListItemSubtitle>
<template #append> <template #append>
<div> <div>
<h4 class="font-weight-medium"> <h4 class="font-weight-medium">
{{ item.status }} {{ item.status }}
</h4> </h4>
</div> </div>
</template> </template>
</VListItem> </VListItem>
<VListItem v-if="schedulerList.length === 0"> <VListItem v-if="schedulerList.length === 0">
<VListItemTitle class="text-center"> {{ t('dashboard.noSchedulers') }} </VListItemTitle> <VListItemTitle class="text-center"> {{ t('dashboard.noSchedulers') }} </VListItemTitle>
</VListItem> </VListItem>
</VList> </VList>
</VCardText> </VCardText>
</VCard> </VCard>
</template>
</VHover>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -87,41 +87,37 @@ const { loading } = useDataRefresh(
</script> </script>
<template> <template>
<VHover> <VCard class="dashboard-work-card">
<template #default="hover"> <VCardItem>
<VCard v-bind="hover.props" class="dashboard-work-card"> <VCardTitle>{{ t('dashboard.realTimeSpeed') }}</VCardTitle>
<VCardItem> </VCardItem>
<VCardTitle>{{ t('dashboard.realTimeSpeed') }}</VCardTitle>
</VCardItem>
<VCardText class="dashboard-work-content pt-4"> <VCardText class="dashboard-work-content pt-4">
<div> <div>
<p class="text-h5 me-2">{{ formatFileSize(downloadInfo.upload_speed) }}/s</p> <p class="text-h5 me-2">{{ formatFileSize(downloadInfo.upload_speed) }}/s</p>
<p class="text-h4 me-2">{{ formatFileSize(downloadInfo.download_speed) }}/s</p> <p class="text-h4 me-2">{{ formatFileSize(downloadInfo.download_speed) }}/s</p>
</div> </div>
<VList class="card-list mt-9"> <VList class="card-list mt-9">
<VListItem v-for="item in infoItems" :key="item.title"> <VListItem v-for="item in infoItems" :key="item.title">
<template #prepend> <template #prepend>
<VIcon rounded :icon="item.avatar" /> <VIcon rounded :icon="item.avatar" />
</template> </template>
<VListItemTitle class="text-sm font-weight-medium mb-1"> <VListItemTitle class="text-sm font-weight-medium mb-1">
{{ item.title }} {{ item.title }}
</VListItemTitle> </VListItemTitle>
<template #append> <template #append>
<div> <div>
<h6 class="text-sm font-weight-medium mb-2"> <h6 class="text-sm font-weight-medium mb-2">
{{ item.amount }} {{ item.amount }}
</h6> </h6>
</div> </div>
</template> </template>
</VListItem> </VListItem>
</VList> </VList>
</VCardText> </VCardText>
</VCard> </VCard>
</template>
</VHover>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -47,28 +47,24 @@ onActivated(() => {
</script> </script>
<template> <template>
<VHover> <VCard class="dashboard-summary-card">
<template #default="hover"> <!-- Triangle Background -->
<VCard v-bind="hover.props" class="dashboard-summary-card"> <VImg :src="triangleBg" class="triangle-bg flip-in-rtl" />
<!-- Triangle Background --> <VCardItem>
<VImg :src="triangleBg" class="triangle-bg flip-in-rtl" /> <VCardTitle>{{ t('dashboard.storage') }}</VCardTitle>
<VCardItem> </VCardItem>
<VCardTitle>{{ t('dashboard.storage') }}</VCardTitle> <VCardText>
</VCardItem> <h5 class="text-2xl font-weight-medium text-primary">
<VCardText> {{ formatFileSize(storage) }}
<h5 class="text-2xl font-weight-medium text-primary"> </h5>
{{ formatFileSize(storage) }} <p class="mt-2">{{ t('storage.usedPercent', { percent: usedPercent }) }} 🚀</p>
</h5> <p class="mt-1">
<p class="mt-2">{{ t('storage.usedPercent', { percent: usedPercent }) }} 🚀</p> <VProgressLinear :model-value="usedPercent" color="primary" />
<p class="mt-1"> </p>
<VProgressLinear :model-value="usedPercent" color="primary" /> </VCardText>
</p> <!-- Trophy -->
</VCardText> <VImg :src="trophy" class="trophy" />
<!-- Trophy --> </VCard>
<VImg :src="trophy" class="trophy" />
</VCard>
</template>
</VHover>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -131,29 +131,25 @@ onActivated(() => {
</script> </script>
<template> <template>
<VHover> <VCard class="dashboard-work-card">
<template #default="hover"> <VCardItem>
<VCard v-bind="hover.props" class="dashboard-work-card"> <VCardTitle>{{ t('dashboard.weeklyOverview') }}</VCardTitle>
<VCardItem> </VCardItem>
<VCardTitle>{{ t('dashboard.weeklyOverview') }}</VCardTitle>
</VCardItem>
<VCardText class="dashboard-work-content"> <VCardText class="dashboard-work-content">
<div class="dashboard-work-chart"> <div class="dashboard-work-chart">
<VApexChart type="bar" :options="options" :series="series" height="100%" /> <VApexChart type="bar" :options="options" :series="series" height="100%" />
</div> </div>
<div class="d-flex align-center mb-3"> <div class="d-flex align-center mb-3">
<h5 class="text-h5 me-4"> <h5 class="text-h5 me-4">
{{ totalCount }} {{ totalCount }}
</h5> </h5>
<p>{{ t('dashboard.weeklyOverviewDescription', { count: totalCount }) }} 😎</p> <p>{{ t('dashboard.weeklyOverviewDescription', { count: totalCount }) }} 😎</p>
</div> </div>
<VBtn v-if="superUser" block to="/history"> {{ t('common.viewDetails') }} </VBtn> <VBtn v-if="superUser" block to="/history"> {{ t('common.viewDetails') }} </VBtn>
</VCardText> </VCardText>
</VCard> </VCard>
</template>
</VHover>
</template> </template>
<style scoped> <style scoped>

View File

@@ -58,29 +58,25 @@ onActivated(() => {
<template> <template>
<div> <div>
<VHover v-for="(data, name) in latestList" :key="name"> <VCard v-for="(data, name) in latestList" :key="name" class="dashboard-work-card dashboard-media-card">
<template #default="hover"> <VCardItem>
<VCard v-bind="hover.props" class="dashboard-work-card dashboard-media-card"> <VCardTitle>{{ t('dashboard.latest') }} - {{ name }}</VCardTitle>
<VCardItem> </VCardItem>
<VCardTitle>{{ t('dashboard.latest') }} - {{ name }}</VCardTitle>
</VCardItem>
<div class="dashboard-card-grid-wrap"> <div class="dashboard-card-grid-wrap">
<ProgressiveCardGrid <ProgressiveCardGrid
:items="data" :items="data"
:get-item-key="item => item.id || item.link || item.title" :get-item-key="item => item.id || item.link || item.title"
:min-item-width="144" :min-item-width="144"
:item-aspect-ratio="1.5" :item-aspect-ratio="1.5"
tabindex="0" tabindex="0"
> >
<template #default="{ item }"> <template #default="{ item }">
<PosterCard :media="item" /> <PosterCard :media="item" />
</template> </template>
</ProgressiveCardGrid> </ProgressiveCardGrid>
</div> </div>
</VCard> </VCard>
</template>
</VHover>
</div> </div>
</template> </template>

View File

@@ -61,28 +61,24 @@ onActivated(() => {
</script> </script>
<template> <template>
<VHover v-if="libraryList.length > 0"> <VCard v-if="libraryList.length > 0" class="dashboard-media-card">
<template #default="hover"> <VCardItem>
<VCard v-bind="hover.props" class="dashboard-media-card"> <VCardTitle>{{ t('dashboard.library') }}</VCardTitle>
<VCardItem> </VCardItem>
<VCardTitle>{{ t('dashboard.library') }}</VCardTitle> <div class="dashboard-card-grid-wrap">
</VCardItem> <ProgressiveCardGrid
<div class="dashboard-card-grid-wrap"> :items="libraryList"
<ProgressiveCardGrid :get-item-key="item => item.id || item.name"
:items="libraryList" :min-item-width="240"
:get-item-key="item => item.id || item.name" :estimated-item-height="160"
:min-item-width="240" tabindex="0"
:estimated-item-height="160" >
tabindex="0" <template #default="{ item }">
> <LibraryCard :media="item" height="10rem" />
<template #default="{ item }"> </template>
<LibraryCard :media="item" height="10rem" /> </ProgressiveCardGrid>
</template> </div>
</ProgressiveCardGrid> </VCard>
</div>
</VCard>
</template>
</VHover>
</template> </template>
<style scoped> <style scoped>

View File

@@ -61,29 +61,25 @@ onActivated(() => {
</script> </script>
<template> <template>
<VHover v-if="playingList.length > 0"> <VCard v-if="playingList.length > 0" class="dashboard-media-card">
<template #default="hover"> <VCardItem>
<VCard v-bind="hover.props" class="dashboard-media-card"> <VCardTitle>{{ t('dashboard.playing') }}</VCardTitle>
<VCardItem> </VCardItem>
<VCardTitle>{{ t('dashboard.playing') }}</VCardTitle>
</VCardItem>
<div class="dashboard-card-grid-wrap"> <div class="dashboard-card-grid-wrap">
<ProgressiveCardGrid <ProgressiveCardGrid
:items="playingList" :items="playingList"
:get-item-key="item => item.id || item.link || item.title" :get-item-key="item => item.id || item.link || item.title"
:min-item-width="240" :min-item-width="240"
:estimated-item-height="160" :estimated-item-height="160"
tabindex="0" tabindex="0"
> >
<template #default="{ item }"> <template #default="{ item }">
<BackdropCard :media="item" height="10rem" /> <BackdropCard :media="item" height="10rem" />
</template> </template>
</ProgressiveCardGrid> </ProgressiveCardGrid>
</div> </div>
</VCard> </VCard>
</template>
</VHover>
</template> </template>
<style scoped> <style scoped>