mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-06 16:16:42 +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 { isNullOrEmptyObject } from '@/@core/utils'
|
||||||
import { loadRemoteComponent } from '@/utils/federationLoader'
|
import { loadRemoteComponent } from '@/utils/federationLoader'
|
||||||
|
|
||||||
|
type DashboardComponentLoader = () => Promise<any>
|
||||||
|
|
||||||
const DashboardSkeleton = {
|
const DashboardSkeleton = {
|
||||||
setup() {
|
setup() {
|
||||||
const SkeletonLoader = resolveComponent('VSkeletonLoader')
|
const SkeletonLoader = resolveComponent('VSkeletonLoader')
|
||||||
@@ -19,51 +21,59 @@ const asyncDashboardOptions = {
|
|||||||
loadingComponent: DashboardSkeleton,
|
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。
|
// 内置仪表盘按需加载,关闭的卡片不再挤进 dashboard 首屏 chunk。
|
||||||
const AnalyticsStorage = defineAsyncComponent({
|
const AnalyticsStorage = createAsyncDashboardComponent('storage')
|
||||||
loader: () => import('@/views/dashboard/AnalyticsStorage.vue'),
|
const AnalyticsMediaStatistic = createAsyncDashboardComponent('mediaStatistic')
|
||||||
...asyncDashboardOptions,
|
const AnalyticsWeeklyOverview = createAsyncDashboardComponent('weeklyOverview')
|
||||||
})
|
const AnalyticsSpeed = createAsyncDashboardComponent('speed')
|
||||||
const AnalyticsMediaStatistic = defineAsyncComponent({
|
const AnalyticsScheduler = createAsyncDashboardComponent('scheduler')
|
||||||
loader: () => import('@/views/dashboard/AnalyticsMediaStatistic.vue'),
|
const AnalyticsCpu = createAsyncDashboardComponent('cpu')
|
||||||
...asyncDashboardOptions,
|
const AnalyticsMemory = createAsyncDashboardComponent('memory')
|
||||||
})
|
const AnalyticsNetwork = createAsyncDashboardComponent('network')
|
||||||
const AnalyticsWeeklyOverview = defineAsyncComponent({
|
const MediaServerLibrary = createAsyncDashboardComponent('library')
|
||||||
loader: () => import('@/views/dashboard/AnalyticsWeeklyOverview.vue'),
|
const MediaServerPlaying = createAsyncDashboardComponent('playing')
|
||||||
...asyncDashboardOptions,
|
const MediaServerLatest = createAsyncDashboardComponent('latest')
|
||||||
})
|
|
||||||
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 props = defineProps({
|
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')
|
// 插件UI渲染模式 ('vuetify' 或 'vue')
|
||||||
const pluginRenderMode = computed(() => props.config?.render_mode || 'vuetify')
|
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 模式:动态加载的组件
|
// Vue 模式:动态加载的组件
|
||||||
const dynamicPluginComponent = defineAsyncComponent({
|
const dynamicPluginComponent = defineAsyncComponent({
|
||||||
// 工厂函数
|
// 工厂函数
|
||||||
loader: async () => {
|
loader: async () => {
|
||||||
try {
|
try {
|
||||||
if (!props.config?.id) {
|
const module = await loadPluginDashboardComponent()
|
||||||
throw new Error('插件ID不存在')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 动态加载远程组件
|
|
||||||
const module = await loadRemoteComponent(props.config.id, 'Dashboard')
|
|
||||||
|
|
||||||
// 直接返回加载的组件,无需再获取default
|
// 直接返回加载的组件,无需再获取default
|
||||||
return module
|
return module
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载远程组件失败:', 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(() => {
|
onUnmounted(() => {
|
||||||
|
isDashboardElementUnmounted = true
|
||||||
// 组件卸载时禁用刷新状态
|
// 组件卸载时禁用刷新状态
|
||||||
emit('update:refreshStatus', false)
|
emit('update:refreshStatus', false)
|
||||||
})
|
})
|
||||||
@@ -179,9 +251,4 @@ onUnmounted(() => {
|
|||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
min-block-size: 0;
|
min-block-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-plugin-vue-renderer :deep(> *) {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
min-block-size: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+159
-51
@@ -61,6 +61,15 @@ const dashboardGridRef = ref<HTMLElement | null>(null)
|
|||||||
// GridStack 实例
|
// GridStack 实例
|
||||||
const dashboardGrid = shallowRef<GridStack | null>(null)
|
const dashboardGrid = shallowRef<GridStack | null>(null)
|
||||||
|
|
||||||
|
// 仪表板配置是否已完成首次加载,包含插件仪表板配置。
|
||||||
|
const isDashboardConfigLoaded = ref(false)
|
||||||
|
|
||||||
|
// 仪表板是否已完成首次整体渐现。
|
||||||
|
const isDashboardRevealed = ref(false)
|
||||||
|
|
||||||
|
// 已完成组件模块加载的仪表板项目 ID。
|
||||||
|
const loadedDashboardGridItemIds = ref<Set<string>>(new Set())
|
||||||
|
|
||||||
// 是否正在由 Vue 同步 GridStack,避免初始化写入覆盖用户布局
|
// 是否正在由 Vue 同步 GridStack,避免初始化写入覆盖用户布局
|
||||||
const isSyncingDashboardGrid = ref(false)
|
const isSyncingDashboardGrid = ref(false)
|
||||||
|
|
||||||
@@ -77,6 +86,8 @@ 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
|
||||||
let dashboardGridResizeRefreshFrame: number | null = null
|
let dashboardGridResizeRefreshFrame: number | null = null
|
||||||
|
let dashboardRevealFrame: number | null = null
|
||||||
|
let isDashboardRevealPending = false
|
||||||
|
|
||||||
// 是否正在手动缩放组件,避免自动测高抢回用户拖动中的高度。
|
// 是否正在手动缩放组件,避免自动测高抢回用户拖动中的高度。
|
||||||
const isDashboardGridResizing = ref(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 可接受的整数区间。
|
// 将未知数值限制到 GridStack 可接受的整数区间。
|
||||||
function clampGridNumber(value: unknown, min: number, max: number, fallback: number) {
|
function clampGridNumber(value: unknown, min: number, max: number, fallback: number) {
|
||||||
const numericValue = Number(value)
|
const numericValue = Number(value)
|
||||||
@@ -486,7 +570,7 @@ async function saveDashboardConfig(payload?: { enabled?: Record<string, boolean>
|
|||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
// 保存后重新获取插件仪表板
|
// 保存后重新获取插件仪表板
|
||||||
getPluginDashboardMeta()
|
void getPluginDashboardMeta()
|
||||||
settingsDialogController?.close()
|
settingsDialogController?.close()
|
||||||
settingsDialogController = null
|
settingsDialogController = null
|
||||||
}
|
}
|
||||||
@@ -501,16 +585,19 @@ function buildPluginDashboardId(plugin_id: string, key: string) {
|
|||||||
async function getPluginDashboardMeta() {
|
async function getPluginDashboardMeta() {
|
||||||
// 只有超级用户才能获取
|
// 只有超级用户才能获取
|
||||||
if (!superUser) return
|
if (!superUser) return
|
||||||
pluginDashboardMeta.value = await api.get('/plugin/dashboard/meta')
|
|
||||||
try {
|
try {
|
||||||
|
pluginDashboardMeta.value = (await api.get('/plugin/dashboard/meta')) ?? []
|
||||||
if (!isNullOrEmptyObject(pluginDashboardMeta.value)) {
|
if (!isNullOrEmptyObject(pluginDashboardMeta.value)) {
|
||||||
// 下载插件仪表板配置
|
// 下载插件仪表板配置
|
||||||
pluginDashboardMeta.value.forEach(async (pluginDashboard: { id: string; key: string }) => {
|
await Promise.all(
|
||||||
const pluginDashboardId = buildPluginDashboardId(pluginDashboard.id, pluginDashboard.key)
|
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)
|
pluginDashboardRefreshStatus.value[pluginDashboardId] = true
|
||||||
})
|
await getPluginDashboard(pluginDashboard.id, pluginDashboard.key)
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -535,7 +622,7 @@ function schedulePluginDashboardRefresh(item: DashboardItem) {
|
|||||||
isRequest.value
|
isRequest.value
|
||||||
) {
|
) {
|
||||||
refreshTimers.value[pluginDashboardId] = setTimeout(() => {
|
refreshTimers.value[pluginDashboardId] = setTimeout(() => {
|
||||||
getPluginDashboard(item.id, item.key)
|
void getPluginDashboard(item.id, item.key)
|
||||||
}, item.attrs.refresh * 1000)
|
}, item.attrs.refresh * 1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -546,7 +633,7 @@ function refreshEnabledPluginDashboards() {
|
|||||||
pluginDashboardMeta.value.forEach((pluginDashboard: { id: string; key: string }) => {
|
pluginDashboardMeta.value.forEach((pluginDashboard: { id: string; key: string }) => {
|
||||||
const pluginDashboardId = buildPluginDashboardId(pluginDashboard.id, pluginDashboard.key)
|
const pluginDashboardId = buildPluginDashboardId(pluginDashboard.id, pluginDashboard.key)
|
||||||
if (enableConfig.value[pluginDashboardId]) {
|
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) {
|
async function getPluginDashboard(id: string, key: string) {
|
||||||
try {
|
try {
|
||||||
const url = key ? `/plugin/dashboard/${id}/${key}` : `/plugin/dashboard/${id}`
|
const url = key ? `/plugin/dashboard/${id}/${key}` : `/plugin/dashboard/${id}`
|
||||||
api.get(url).then((res: any) => {
|
const res: DashboardItem | undefined = await api.get(url)
|
||||||
if (res) {
|
if (res) {
|
||||||
// 名称替换为元信息的名称
|
// 名称替换为元信息的名称
|
||||||
const meta = pluginDashboardMeta.value.find(
|
const meta = pluginDashboardMeta.value.find(
|
||||||
(item: { id: string; key: string }) => item.id === id && item.key === key,
|
(item: { id: string; key: string }) => item.id === id && item.key === key,
|
||||||
)
|
)
|
||||||
if (meta) res.name = meta.name
|
if (meta) res.name = meta.name
|
||||||
// 保存到仪表板配置中,如果已经存在则替换
|
// 保存到仪表板配置中,如果已经存在则替换
|
||||||
const index = dashboardConfigs.value.findIndex(
|
const index = dashboardConfigs.value.findIndex(
|
||||||
(item: { id: string; key: string }) => item.id === id && item.key === key,
|
(item: { id: string; key: string }) => item.id === id && item.key === key,
|
||||||
)
|
)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
dashboardConfigs.value[index] = res
|
dashboardConfigs.value[index] = res
|
||||||
} else {
|
} else {
|
||||||
dashboardConfigs.value.push(res)
|
dashboardConfigs.value.push(res)
|
||||||
// 为新增的插件仪表板生成颜色
|
// 为新增的插件仪表板生成颜色
|
||||||
const pluginDashboardId = buildPluginDashboardId(id, key)
|
|
||||||
if (!itemColors.value[pluginDashboardId]) {
|
|
||||||
itemColors.value[pluginDashboardId] = getItemColor(pluginDashboardId)
|
|
||||||
}
|
|
||||||
// 排序
|
|
||||||
sortDashboardConfigs()
|
|
||||||
}
|
|
||||||
const pluginDashboardId = buildPluginDashboardId(id, key)
|
const pluginDashboardId = buildPluginDashboardId(id, key)
|
||||||
// 定时刷新
|
if (!itemColors.value[pluginDashboardId]) {
|
||||||
schedulePluginDashboardRefresh(res)
|
itemColors.value[pluginDashboardId] = getItemColor(pluginDashboardId)
|
||||||
|
}
|
||||||
|
// 排序
|
||||||
|
sortDashboardConfigs()
|
||||||
}
|
}
|
||||||
})
|
// 定时刷新
|
||||||
|
schedulePluginDashboardRefresh(res)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
@@ -689,7 +774,10 @@ async function syncDashboardGrid() {
|
|||||||
grid.batchUpdate(false)
|
grid.batchUpdate(false)
|
||||||
updateDashboardGridEditableState(isLayoutEditing.value)
|
updateDashboardGridEditableState(isLayoutEditing.value)
|
||||||
observeDashboardGridContent()
|
observeDashboardGridContent()
|
||||||
nextTick(resizeAutoDashboardItemsToContent)
|
nextTick(() => {
|
||||||
|
resizeAutoDashboardItemsToContent()
|
||||||
|
scheduleDashboardReveal()
|
||||||
|
})
|
||||||
} finally {
|
} finally {
|
||||||
isSyncingDashboardGrid.value = false
|
isSyncingDashboardGrid.value = false
|
||||||
}
|
}
|
||||||
@@ -731,8 +819,7 @@ function shouldScheduleDashboardContentResize(element: GridItemHTMLElement, next
|
|||||||
dashboardGridObservedContentHeights.set(id, nextHeight)
|
dashboardGridObservedContentHeights.set(id, nextHeight)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
previousHeight === undefined ||
|
previousHeight === undefined || Math.abs(nextHeight - previousHeight) >= DASHBOARD_GRID_CONTENT_RESIZE_THRESHOLD
|
||||||
Math.abs(nextHeight - previousHeight) >= DASHBOARD_GRID_CONTENT_RESIZE_THRESHOLD
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -865,7 +952,9 @@ watch(isLayoutEditing, value => {
|
|||||||
watch(
|
watch(
|
||||||
dashboardGridItems,
|
dashboardGridItems,
|
||||||
() => {
|
() => {
|
||||||
|
syncDashboardLoadedItemIds()
|
||||||
syncDashboardGrid()
|
syncDashboardGrid()
|
||||||
|
scheduleDashboardReveal()
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
@@ -873,7 +962,9 @@ watch(
|
|||||||
onBeforeMount(async () => {
|
onBeforeMount(async () => {
|
||||||
await loadDashboardConfig()
|
await loadDashboardConfig()
|
||||||
initializeColors()
|
initializeColors()
|
||||||
getPluginDashboardMeta()
|
await getPluginDashboardMeta()
|
||||||
|
isDashboardConfigLoaded.value = true
|
||||||
|
scheduleDashboardReveal()
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -903,6 +994,10 @@ onBeforeUnmount(() => {
|
|||||||
cancelAnimationFrame(dashboardGridResizeRefreshFrame)
|
cancelAnimationFrame(dashboardGridResizeRefreshFrame)
|
||||||
dashboardGridResizeRefreshFrame = null
|
dashboardGridResizeRefreshFrame = null
|
||||||
}
|
}
|
||||||
|
if (dashboardRevealFrame !== null) {
|
||||||
|
cancelAnimationFrame(dashboardRevealFrame)
|
||||||
|
dashboardRevealFrame = null
|
||||||
|
}
|
||||||
dashboardGridPendingContentResize.clear()
|
dashboardGridPendingContentResize.clear()
|
||||||
dashboardGridObservedContentHeights.clear()
|
dashboardGridObservedContentHeights.clear()
|
||||||
dashboardGridResizeStartHeights.clear()
|
dashboardGridResizeStartHeights.clear()
|
||||||
@@ -913,7 +1008,11 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<template>
|
<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
|
<div
|
||||||
v-for="gridItem in dashboardGridItems"
|
v-for="gridItem in dashboardGridItems"
|
||||||
:key="gridItem.id"
|
:key="gridItem.id"
|
||||||
@@ -935,6 +1034,7 @@ onBeforeUnmount(() => {
|
|||||||
:config="gridItem.config"
|
:config="gridItem.config"
|
||||||
:allow-refresh="isRequest"
|
:allow-refresh="isRequest"
|
||||||
v-model:refreshStatus="pluginDashboardRefreshStatus[gridItem.id]"
|
v-model:refreshStatus="pluginDashboardRefreshStatus[gridItem.id]"
|
||||||
|
@loaded="markDashboardGridItemLoaded(gridItem.id)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="isLayoutEditing" class="dashboard-grid-drag-handle" :aria-label="t('dashboard.dragHandle')">
|
<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 */
|
/* stylelint-disable selector-pseudo-class-no-unknown */
|
||||||
|
|
||||||
.dashboard-grid {
|
.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) {
|
.dashboard-grid.is-ready {
|
||||||
overflow: hidden;
|
opacity: 1;
|
||||||
box-shadow: var(--app-surface-shadow) !important;
|
pointer-events: auto;
|
||||||
}
|
transform: translateY(0);
|
||||||
|
|
||||||
@media (hover: hover) {
|
|
||||||
.dashboard-grid :deep(.v-card:hover) {
|
|
||||||
box-shadow: var(--app-surface-hover-shadow) !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-grid-item.is-manual-height :deep(.v-card) {
|
.dashboard-grid-item.is-manual-height :deep(.v-card) {
|
||||||
@@ -1090,4 +1191,11 @@ onBeforeUnmount(() => {
|
|||||||
inset-block-end: -4px;
|
inset-block-end: -4px;
|
||||||
inset-inline-end: -4px;
|
inset-inline-end: -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.dashboard-grid {
|
||||||
|
transform: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -159,9 +159,4 @@ useKeepAliveRefresh(refresh)
|
|||||||
min-block-size: 0;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -163,10 +163,4 @@ useKeepAliveRefresh(refresh)
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-block-size: 0;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -206,9 +206,4 @@ useKeepAliveRefresh(refresh)
|
|||||||
min-block-size: 0;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -164,10 +164,4 @@ onActivated(() => {
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-block-size: 0;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -100,8 +100,4 @@ onActivated(() => {
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-block-size: 0;
|
min-block-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-media-grid :deep(.progressive-card-grid__track) {
|
|
||||||
min-block-size: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -89,8 +89,4 @@ onActivated(() => {
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-block-size: 0;
|
min-block-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-media-grid :deep(.progressive-card-grid__track) {
|
|
||||||
min-block-size: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -90,8 +90,4 @@ onActivated(() => {
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-block-size: 0;
|
min-block-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-media-grid :deep(.progressive-card-grid__track) {
|
|
||||||
min-block-size: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user