mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-07-02 04:51:30 +08:00
fix(dashboard): stabilize editable layout controls
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user