mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-16 13:11:22 +08:00
feat(dashboard): 优化仪表盘组件加载逻辑,支持异步加载和状态管理
This commit is contained in:
@@ -6,6 +6,8 @@ import DashboardRender from '@/components/render/DashboardRender.vue'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
import { loadRemoteComponent } from '@/utils/federationLoader'
|
||||
|
||||
type DashboardComponentLoader = () => Promise<any>
|
||||
|
||||
const DashboardSkeleton = {
|
||||
setup() {
|
||||
const SkeletonLoader = resolveComponent('VSkeletonLoader')
|
||||
@@ -19,51 +21,59 @@ const asyncDashboardOptions = {
|
||||
loadingComponent: DashboardSkeleton,
|
||||
}
|
||||
|
||||
const builtInDashboardComponentLoaders: Record<string, DashboardComponentLoader> = {
|
||||
storage: () => import('@/views/dashboard/AnalyticsStorage.vue'),
|
||||
mediaStatistic: () => import('@/views/dashboard/AnalyticsMediaStatistic.vue'),
|
||||
weeklyOverview: () => import('@/views/dashboard/AnalyticsWeeklyOverview.vue'),
|
||||
speed: () => import('@/views/dashboard/AnalyticsSpeed.vue'),
|
||||
scheduler: () => import('@/views/dashboard/AnalyticsScheduler.vue'),
|
||||
cpu: () => import('@/views/dashboard/AnalyticsCpu.vue'),
|
||||
memory: () => import('@/views/dashboard/AnalyticsMemory.vue'),
|
||||
network: () => import('@/views/dashboard/AnalyticsNetwork.vue'),
|
||||
library: () => import('@/views/dashboard/MediaServerLibrary.vue'),
|
||||
playing: () => import('@/views/dashboard/MediaServerPlaying.vue'),
|
||||
latest: () => import('@/views/dashboard/MediaServerLatest.vue'),
|
||||
}
|
||||
|
||||
const builtInDashboardComponentPromises = new Map<string, Promise<any>>()
|
||||
|
||||
// 复用内置仪表盘组件加载 Promise,让页面层可以等待异步组件模块真正加载完成。
|
||||
function loadBuiltInDashboardComponent(id: string) {
|
||||
const loader = builtInDashboardComponentLoaders[id]
|
||||
if (!loader) return Promise.resolve()
|
||||
|
||||
let loadPromise = builtInDashboardComponentPromises.get(id)
|
||||
if (!loadPromise) {
|
||||
loadPromise = loader().catch(error => {
|
||||
builtInDashboardComponentPromises.delete(id)
|
||||
throw error
|
||||
})
|
||||
builtInDashboardComponentPromises.set(id, loadPromise)
|
||||
}
|
||||
|
||||
return loadPromise
|
||||
}
|
||||
|
||||
// 创建内置仪表盘异步组件,并与加载完成上报共享同一份加载 Promise。
|
||||
function createAsyncDashboardComponent(id: string) {
|
||||
return defineAsyncComponent({
|
||||
loader: () => loadBuiltInDashboardComponent(id),
|
||||
...asyncDashboardOptions,
|
||||
})
|
||||
}
|
||||
|
||||
// 内置仪表盘按需加载,关闭的卡片不再挤进 dashboard 首屏 chunk。
|
||||
const AnalyticsStorage = defineAsyncComponent({
|
||||
loader: () => import('@/views/dashboard/AnalyticsStorage.vue'),
|
||||
...asyncDashboardOptions,
|
||||
})
|
||||
const AnalyticsMediaStatistic = defineAsyncComponent({
|
||||
loader: () => import('@/views/dashboard/AnalyticsMediaStatistic.vue'),
|
||||
...asyncDashboardOptions,
|
||||
})
|
||||
const AnalyticsWeeklyOverview = defineAsyncComponent({
|
||||
loader: () => import('@/views/dashboard/AnalyticsWeeklyOverview.vue'),
|
||||
...asyncDashboardOptions,
|
||||
})
|
||||
const AnalyticsSpeed = defineAsyncComponent({
|
||||
loader: () => import('@/views/dashboard/AnalyticsSpeed.vue'),
|
||||
...asyncDashboardOptions,
|
||||
})
|
||||
const AnalyticsScheduler = defineAsyncComponent({
|
||||
loader: () => import('@/views/dashboard/AnalyticsScheduler.vue'),
|
||||
...asyncDashboardOptions,
|
||||
})
|
||||
const AnalyticsCpu = defineAsyncComponent({
|
||||
loader: () => import('@/views/dashboard/AnalyticsCpu.vue'),
|
||||
...asyncDashboardOptions,
|
||||
})
|
||||
const AnalyticsMemory = defineAsyncComponent({
|
||||
loader: () => import('@/views/dashboard/AnalyticsMemory.vue'),
|
||||
...asyncDashboardOptions,
|
||||
})
|
||||
const AnalyticsNetwork = defineAsyncComponent({
|
||||
loader: () => import('@/views/dashboard/AnalyticsNetwork.vue'),
|
||||
...asyncDashboardOptions,
|
||||
})
|
||||
const MediaServerLibrary = defineAsyncComponent({
|
||||
loader: () => import('@/views/dashboard/MediaServerLibrary.vue'),
|
||||
...asyncDashboardOptions,
|
||||
})
|
||||
const MediaServerPlaying = defineAsyncComponent({
|
||||
loader: () => import('@/views/dashboard/MediaServerPlaying.vue'),
|
||||
...asyncDashboardOptions,
|
||||
})
|
||||
const MediaServerLatest = defineAsyncComponent({
|
||||
loader: () => import('@/views/dashboard/MediaServerLatest.vue'),
|
||||
...asyncDashboardOptions,
|
||||
})
|
||||
const AnalyticsStorage = createAsyncDashboardComponent('storage')
|
||||
const AnalyticsMediaStatistic = createAsyncDashboardComponent('mediaStatistic')
|
||||
const AnalyticsWeeklyOverview = createAsyncDashboardComponent('weeklyOverview')
|
||||
const AnalyticsSpeed = createAsyncDashboardComponent('speed')
|
||||
const AnalyticsScheduler = createAsyncDashboardComponent('scheduler')
|
||||
const AnalyticsCpu = createAsyncDashboardComponent('cpu')
|
||||
const AnalyticsMemory = createAsyncDashboardComponent('memory')
|
||||
const AnalyticsNetwork = createAsyncDashboardComponent('network')
|
||||
const MediaServerLibrary = createAsyncDashboardComponent('library')
|
||||
const MediaServerPlaying = createAsyncDashboardComponent('playing')
|
||||
const MediaServerLatest = createAsyncDashboardComponent('latest')
|
||||
|
||||
// 输入参数
|
||||
const props = defineProps({
|
||||
@@ -78,27 +88,43 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:refreshStatus'])
|
||||
const emit = defineEmits(['update:refreshStatus', 'loaded'])
|
||||
|
||||
// 当前仪表盘节点是否已经向页面层报告过加载完成。
|
||||
const isDashboardElementLoaded = ref(false)
|
||||
|
||||
let isDashboardElementUnmounted = false
|
||||
let pluginDashboardComponentLoadPromise: Promise<any> | null = null
|
||||
|
||||
// 插件UI渲染模式 ('vuetify' 或 'vue')
|
||||
const pluginRenderMode = computed(() => props.config?.render_mode || 'vuetify')
|
||||
|
||||
// 加载 Vue 模式的插件仪表盘远程组件,并缓存当前节点的加载 Promise。
|
||||
function loadPluginDashboardComponent() {
|
||||
if (!props.config?.id) return Promise.reject(new Error('插件ID不存在'))
|
||||
|
||||
if (!pluginDashboardComponentLoadPromise) {
|
||||
pluginDashboardComponentLoadPromise = loadRemoteComponent(props.config.id, 'Dashboard').catch(error => {
|
||||
pluginDashboardComponentLoadPromise = null
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
return pluginDashboardComponentLoadPromise
|
||||
}
|
||||
|
||||
// Vue 模式:动态加载的组件
|
||||
const dynamicPluginComponent = defineAsyncComponent({
|
||||
// 工厂函数
|
||||
loader: async () => {
|
||||
try {
|
||||
if (!props.config?.id) {
|
||||
throw new Error('插件ID不存在')
|
||||
}
|
||||
|
||||
// 动态加载远程组件
|
||||
const module = await loadRemoteComponent(props.config.id, 'Dashboard')
|
||||
const module = await loadPluginDashboardComponent()
|
||||
|
||||
// 直接返回加载的组件,无需再获取default
|
||||
return module
|
||||
} catch (error) {
|
||||
console.error('加载远程组件失败:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
// 加载中显示的组件
|
||||
@@ -115,7 +141,53 @@ const dynamicPluginComponent = defineAsyncComponent({
|
||||
},
|
||||
})
|
||||
|
||||
// 判断当前配置是否对应内置异步仪表盘组件。
|
||||
function isBuiltInDashboardElement() {
|
||||
return !!props.config?.id && !!builtInDashboardComponentLoaders[props.config.id]
|
||||
}
|
||||
|
||||
// 判断当前配置是否需要等待插件 Vue 远程组件加载。
|
||||
function isVuePluginDashboardElement() {
|
||||
return !isBuiltInDashboardElement() && pluginRenderMode.value === 'vue' && !isNullOrEmptyObject(props.config)
|
||||
}
|
||||
|
||||
// 向页面层上报当前仪表盘节点已完成首次组件加载。
|
||||
function emitDashboardElementLoaded() {
|
||||
if (isDashboardElementLoaded.value || isDashboardElementUnmounted) return
|
||||
|
||||
isDashboardElementLoaded.value = true
|
||||
emit('loaded')
|
||||
}
|
||||
|
||||
// 等待当前仪表盘节点的异步组件加载完成,静态渲染模式则等待一次 DOM 更新。
|
||||
async function waitForDashboardElementLoaded() {
|
||||
if (isDashboardElementLoaded.value) return
|
||||
|
||||
try {
|
||||
if (isBuiltInDashboardElement() && props.config?.id) {
|
||||
await loadBuiltInDashboardComponent(props.config.id)
|
||||
} else if (isVuePluginDashboardElement()) {
|
||||
await loadPluginDashboardComponent()
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
emitDashboardElementLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.config?.id, props.config?.key, pluginRenderMode.value],
|
||||
() => {
|
||||
void waitForDashboardElementLoaded()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
isDashboardElementUnmounted = true
|
||||
// 组件卸载时禁用刷新状态
|
||||
emit('update:refreshStatus', false)
|
||||
})
|
||||
@@ -179,9 +251,4 @@ onUnmounted(() => {
|
||||
inline-size: 100%;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.dashboard-plugin-vue-renderer :deep(> *) {
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -61,6 +61,15 @@ const dashboardGridRef = ref<HTMLElement | null>(null)
|
||||
// GridStack 实例
|
||||
const dashboardGrid = shallowRef<GridStack | null>(null)
|
||||
|
||||
// 仪表板配置是否已完成首次加载,包含插件仪表板配置。
|
||||
const isDashboardConfigLoaded = ref(false)
|
||||
|
||||
// 仪表板是否已完成首次整体渐现。
|
||||
const isDashboardRevealed = ref(false)
|
||||
|
||||
// 已完成组件模块加载的仪表板项目 ID。
|
||||
const loadedDashboardGridItemIds = ref<Set<string>>(new Set())
|
||||
|
||||
// 是否正在由 Vue 同步 GridStack,避免初始化写入覆盖用户布局
|
||||
const isSyncingDashboardGrid = ref(false)
|
||||
|
||||
@@ -77,6 +86,8 @@ const dashboardGridObservedContentHeights = new Map<string, number>()
|
||||
let dashboardGridContentObserver: ResizeObserver | null = null
|
||||
let dashboardGridContentResizeFrame: number | null = null
|
||||
let dashboardGridResizeRefreshFrame: number | null = null
|
||||
let dashboardRevealFrame: number | null = null
|
||||
let isDashboardRevealPending = false
|
||||
|
||||
// 是否正在手动缩放组件,避免自动测高抢回用户拖动中的高度。
|
||||
const isDashboardGridResizing = ref(false)
|
||||
@@ -226,6 +237,79 @@ const dashboardGridItems = computed<DashboardGridItem[]>(() =>
|
||||
}),
|
||||
)
|
||||
|
||||
// 获取当前可渲染仪表板项目 ID 列表。
|
||||
function getDashboardGridItemIds() {
|
||||
return dashboardGridItems.value.map(item => item.id)
|
||||
}
|
||||
|
||||
// 清理已经不在当前仪表板列表中的加载完成标记。
|
||||
function syncDashboardLoadedItemIds() {
|
||||
const currentIds = new Set(getDashboardGridItemIds())
|
||||
const nextLoadedIds = new Set([...loadedDashboardGridItemIds.value].filter(id => currentIds.has(id)))
|
||||
|
||||
if (nextLoadedIds.size !== loadedDashboardGridItemIds.value.size) {
|
||||
loadedDashboardGridItemIds.value = nextLoadedIds
|
||||
}
|
||||
}
|
||||
|
||||
// 判断当前启用的仪表板项目是否都已经完成组件加载。
|
||||
function areDashboardGridItemsLoaded() {
|
||||
return getDashboardGridItemIds().every(id => loadedDashboardGridItemIds.value.has(id))
|
||||
}
|
||||
|
||||
// 判断 GridStack 是否已经可承载当前仪表板项目。
|
||||
function isDashboardGridReadyForReveal() {
|
||||
return getDashboardGridItemIds().length === 0 || !!dashboardGrid.value
|
||||
}
|
||||
|
||||
// 在配置、组件和 GridStack 都就绪后安排仪表板整体渐现。
|
||||
function scheduleDashboardReveal() {
|
||||
if (
|
||||
isDashboardRevealed.value ||
|
||||
isDashboardRevealPending ||
|
||||
dashboardRevealFrame !== null ||
|
||||
!isDashboardConfigLoaded.value ||
|
||||
!isDashboardGridReadyForReveal() ||
|
||||
!areDashboardGridItemsLoaded()
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
isDashboardRevealPending = true
|
||||
void nextTick(() => {
|
||||
isDashboardRevealPending = false
|
||||
if (
|
||||
isDashboardRevealed.value ||
|
||||
!isDashboardConfigLoaded.value ||
|
||||
!isDashboardGridReadyForReveal() ||
|
||||
!areDashboardGridItemsLoaded()
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
resizeAutoDashboardItemsToContent()
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
isDashboardRevealed.value = true
|
||||
return
|
||||
}
|
||||
|
||||
dashboardRevealFrame = window.requestAnimationFrame(() => {
|
||||
dashboardRevealFrame = null
|
||||
isDashboardRevealed.value = true
|
||||
notifyDashboardContentResize()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 标记单个仪表板项目已经完成首次组件加载。
|
||||
function markDashboardGridItemLoaded(id: string) {
|
||||
if (loadedDashboardGridItemIds.value.has(id)) return
|
||||
|
||||
loadedDashboardGridItemIds.value = new Set([...loadedDashboardGridItemIds.value, id])
|
||||
scheduleDashboardReveal()
|
||||
}
|
||||
|
||||
// 将未知数值限制到 GridStack 可接受的整数区间。
|
||||
function clampGridNumber(value: unknown, min: number, max: number, fallback: number) {
|
||||
const numericValue = Number(value)
|
||||
@@ -486,7 +570,7 @@ async function saveDashboardConfig(payload?: { enabled?: Record<string, boolean>
|
||||
console.error(error)
|
||||
}
|
||||
// 保存后重新获取插件仪表板
|
||||
getPluginDashboardMeta()
|
||||
void getPluginDashboardMeta()
|
||||
settingsDialogController?.close()
|
||||
settingsDialogController = null
|
||||
}
|
||||
@@ -501,16 +585,19 @@ function buildPluginDashboardId(plugin_id: string, key: string) {
|
||||
async function getPluginDashboardMeta() {
|
||||
// 只有超级用户才能获取
|
||||
if (!superUser) return
|
||||
pluginDashboardMeta.value = await api.get('/plugin/dashboard/meta')
|
||||
|
||||
try {
|
||||
pluginDashboardMeta.value = (await api.get('/plugin/dashboard/meta')) ?? []
|
||||
if (!isNullOrEmptyObject(pluginDashboardMeta.value)) {
|
||||
// 下载插件仪表板配置
|
||||
pluginDashboardMeta.value.forEach(async (pluginDashboard: { id: string; key: string }) => {
|
||||
const pluginDashboardId = buildPluginDashboardId(pluginDashboard.id, pluginDashboard.key)
|
||||
// 初始化插件仪表板的刷新状态
|
||||
pluginDashboardRefreshStatus.value[pluginDashboardId] = true
|
||||
await getPluginDashboard(pluginDashboard.id, pluginDashboard.key)
|
||||
})
|
||||
await Promise.all(
|
||||
pluginDashboardMeta.value.map(async (pluginDashboard: { id: string; key: string }) => {
|
||||
const pluginDashboardId = buildPluginDashboardId(pluginDashboard.id, pluginDashboard.key)
|
||||
// 初始化插件仪表板的刷新状态
|
||||
pluginDashboardRefreshStatus.value[pluginDashboardId] = true
|
||||
await getPluginDashboard(pluginDashboard.id, pluginDashboard.key)
|
||||
}),
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
@@ -535,7 +622,7 @@ function schedulePluginDashboardRefresh(item: DashboardItem) {
|
||||
isRequest.value
|
||||
) {
|
||||
refreshTimers.value[pluginDashboardId] = setTimeout(() => {
|
||||
getPluginDashboard(item.id, item.key)
|
||||
void getPluginDashboard(item.id, item.key)
|
||||
}, item.attrs.refresh * 1000)
|
||||
}
|
||||
}
|
||||
@@ -546,7 +633,7 @@ function refreshEnabledPluginDashboards() {
|
||||
pluginDashboardMeta.value.forEach((pluginDashboard: { id: string; key: string }) => {
|
||||
const pluginDashboardId = buildPluginDashboardId(pluginDashboard.id, pluginDashboard.key)
|
||||
if (enableConfig.value[pluginDashboardId]) {
|
||||
getPluginDashboard(pluginDashboard.id, pluginDashboard.key)
|
||||
void getPluginDashboard(pluginDashboard.id, pluginDashboard.key)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -555,34 +642,32 @@ function refreshEnabledPluginDashboards() {
|
||||
async function getPluginDashboard(id: string, key: string) {
|
||||
try {
|
||||
const url = key ? `/plugin/dashboard/${id}/${key}` : `/plugin/dashboard/${id}`
|
||||
api.get(url).then((res: any) => {
|
||||
if (res) {
|
||||
// 名称替换为元信息的名称
|
||||
const meta = pluginDashboardMeta.value.find(
|
||||
(item: { id: string; key: string }) => item.id === id && item.key === key,
|
||||
)
|
||||
if (meta) res.name = meta.name
|
||||
// 保存到仪表板配置中,如果已经存在则替换
|
||||
const index = dashboardConfigs.value.findIndex(
|
||||
(item: { id: string; key: string }) => item.id === id && item.key === key,
|
||||
)
|
||||
if (index !== -1) {
|
||||
dashboardConfigs.value[index] = res
|
||||
} else {
|
||||
dashboardConfigs.value.push(res)
|
||||
// 为新增的插件仪表板生成颜色
|
||||
const pluginDashboardId = buildPluginDashboardId(id, key)
|
||||
if (!itemColors.value[pluginDashboardId]) {
|
||||
itemColors.value[pluginDashboardId] = getItemColor(pluginDashboardId)
|
||||
}
|
||||
// 排序
|
||||
sortDashboardConfigs()
|
||||
}
|
||||
const res: DashboardItem | undefined = await api.get(url)
|
||||
if (res) {
|
||||
// 名称替换为元信息的名称
|
||||
const meta = pluginDashboardMeta.value.find(
|
||||
(item: { id: string; key: string }) => item.id === id && item.key === key,
|
||||
)
|
||||
if (meta) res.name = meta.name
|
||||
// 保存到仪表板配置中,如果已经存在则替换
|
||||
const index = dashboardConfigs.value.findIndex(
|
||||
(item: { id: string; key: string }) => item.id === id && item.key === key,
|
||||
)
|
||||
if (index !== -1) {
|
||||
dashboardConfigs.value[index] = res
|
||||
} else {
|
||||
dashboardConfigs.value.push(res)
|
||||
// 为新增的插件仪表板生成颜色
|
||||
const pluginDashboardId = buildPluginDashboardId(id, key)
|
||||
// 定时刷新
|
||||
schedulePluginDashboardRefresh(res)
|
||||
if (!itemColors.value[pluginDashboardId]) {
|
||||
itemColors.value[pluginDashboardId] = getItemColor(pluginDashboardId)
|
||||
}
|
||||
// 排序
|
||||
sortDashboardConfigs()
|
||||
}
|
||||
})
|
||||
// 定时刷新
|
||||
schedulePluginDashboardRefresh(res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
@@ -689,7 +774,10 @@ async function syncDashboardGrid() {
|
||||
grid.batchUpdate(false)
|
||||
updateDashboardGridEditableState(isLayoutEditing.value)
|
||||
observeDashboardGridContent()
|
||||
nextTick(resizeAutoDashboardItemsToContent)
|
||||
nextTick(() => {
|
||||
resizeAutoDashboardItemsToContent()
|
||||
scheduleDashboardReveal()
|
||||
})
|
||||
} finally {
|
||||
isSyncingDashboardGrid.value = false
|
||||
}
|
||||
@@ -731,8 +819,7 @@ function shouldScheduleDashboardContentResize(element: GridItemHTMLElement, next
|
||||
dashboardGridObservedContentHeights.set(id, nextHeight)
|
||||
|
||||
return (
|
||||
previousHeight === undefined ||
|
||||
Math.abs(nextHeight - previousHeight) >= DASHBOARD_GRID_CONTENT_RESIZE_THRESHOLD
|
||||
previousHeight === undefined || Math.abs(nextHeight - previousHeight) >= DASHBOARD_GRID_CONTENT_RESIZE_THRESHOLD
|
||||
)
|
||||
}
|
||||
|
||||
@@ -865,7 +952,9 @@ watch(isLayoutEditing, value => {
|
||||
watch(
|
||||
dashboardGridItems,
|
||||
() => {
|
||||
syncDashboardLoadedItemIds()
|
||||
syncDashboardGrid()
|
||||
scheduleDashboardReveal()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
@@ -873,7 +962,9 @@ watch(
|
||||
onBeforeMount(async () => {
|
||||
await loadDashboardConfig()
|
||||
initializeColors()
|
||||
getPluginDashboardMeta()
|
||||
await getPluginDashboardMeta()
|
||||
isDashboardConfigLoaded.value = true
|
||||
scheduleDashboardReveal()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
@@ -903,6 +994,10 @@ onBeforeUnmount(() => {
|
||||
cancelAnimationFrame(dashboardGridResizeRefreshFrame)
|
||||
dashboardGridResizeRefreshFrame = null
|
||||
}
|
||||
if (dashboardRevealFrame !== null) {
|
||||
cancelAnimationFrame(dashboardRevealFrame)
|
||||
dashboardRevealFrame = null
|
||||
}
|
||||
dashboardGridPendingContentResize.clear()
|
||||
dashboardGridObservedContentHeights.clear()
|
||||
dashboardGridResizeStartHeights.clear()
|
||||
@@ -913,7 +1008,11 @@ onBeforeUnmount(() => {
|
||||
|
||||
<template>
|
||||
<!-- 仪表板 -->
|
||||
<div ref="dashboardGridRef" class="grid-stack dashboard-grid" :class="{ 'is-editing': isLayoutEditing }">
|
||||
<div
|
||||
ref="dashboardGridRef"
|
||||
class="grid-stack dashboard-grid"
|
||||
:class="{ 'is-editing': isLayoutEditing, 'is-ready': isDashboardRevealed }"
|
||||
>
|
||||
<div
|
||||
v-for="gridItem in dashboardGridItems"
|
||||
:key="gridItem.id"
|
||||
@@ -935,6 +1034,7 @@ onBeforeUnmount(() => {
|
||||
:config="gridItem.config"
|
||||
:allow-refresh="isRequest"
|
||||
v-model:refreshStatus="pluginDashboardRefreshStatus[gridItem.id]"
|
||||
@loaded="markDashboardGridItemLoaded(gridItem.id)"
|
||||
/>
|
||||
</div>
|
||||
<span v-if="isLayoutEditing" class="dashboard-grid-drag-handle" :aria-label="t('dashboard.dragHandle')">
|
||||
@@ -979,18 +1079,19 @@ onBeforeUnmount(() => {
|
||||
/* stylelint-disable selector-pseudo-class-no-unknown */
|
||||
|
||||
.dashboard-grid {
|
||||
margin-block: -6px 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateY(8px);
|
||||
transition:
|
||||
opacity 0.45s cubic-bezier(0.25, 1, 0.5, 1),
|
||||
transform 0.45s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
.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.is-ready {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.dashboard-grid-item.is-manual-height :deep(.v-card) {
|
||||
@@ -1090,4 +1191,11 @@ onBeforeUnmount(() => {
|
||||
inset-block-end: -4px;
|
||||
inset-inline-end: -4px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dashboard-grid {
|
||||
transform: none;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -159,9 +159,4 @@ useKeepAliveRefresh(refresh)
|
||||
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>
|
||||
|
||||
@@ -163,10 +163,4 @@ useKeepAliveRefresh(refresh)
|
||||
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>
|
||||
|
||||
@@ -206,9 +206,4 @@ useKeepAliveRefresh(refresh)
|
||||
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>
|
||||
|
||||
@@ -164,10 +164,4 @@ onActivated(() => {
|
||||
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>
|
||||
|
||||
@@ -100,8 +100,4 @@ onActivated(() => {
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.dashboard-media-grid :deep(.progressive-card-grid__track) {
|
||||
min-block-size: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -89,8 +89,4 @@ onActivated(() => {
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.dashboard-media-grid :deep(.progressive-card-grid__track) {
|
||||
min-block-size: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -90,8 +90,4 @@ onActivated(() => {
|
||||
flex: 1 1 auto;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.dashboard-media-grid :deep(.progressive-card-grid__track) {
|
||||
min-block-size: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user