fix(dashboard): reconcile plugin dashboard lifecycle (#697)

This commit is contained in:
InfinityPacer
2026-08-23 17:03:04 +08:00
committed by GitHub
parent 17d4dbed1d
commit 5277f148a1
5 changed files with 552 additions and 22 deletions
-5
View File
@@ -612,11 +612,6 @@
"count": 1
}
},
"src/pages/dashboard.vue": {
"@typescript-eslint/no-explicit-any": {
"count": 1
}
},
"src/pages/resource.vue": {
"@typescript-eslint/no-explicit-any": {
"count": 3
@@ -0,0 +1,220 @@
import { useDashboardMediaGridCapacity } from '@/composables/useDashboardMediaGridCapacity'
import { fireEvent, render, screen } from '@testing-library/vue'
import { defineComponent, nextTick, ref, type Ref } from 'vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
class ResizeObserverMock implements ResizeObserver {
static instances: ResizeObserverMock[] = []
readonly targets = new Set<Element>()
readonly disconnect = vi.fn(() => this.targets.clear())
readonly observe = vi.fn((target: Element) => this.targets.add(target))
readonly unobserve = vi.fn((target: Element) => this.targets.delete(target))
constructor(private readonly callback: ResizeObserverCallback) {
ResizeObserverMock.instances.push(this)
}
trigger() {
this.callback([], this)
}
}
interface CapacityHarnessOptions {
contentSelector?: string
horizontalPadding?: number
maxCount?: number
minItemWidth: number
rows?: Ref<number>
}
function createFrameHarness() {
const callbacks = new Map<number, FrameRequestCallback>()
let nextFrameId = 0
vi.spyOn(window, 'requestAnimationFrame').mockImplementation(callback => {
nextFrameId += 1
callbacks.set(nextFrameId, callback)
return nextFrameId
})
const cancelFrame = vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(frameId => {
callbacks.delete(frameId)
})
return {
callbacks,
cancelFrame,
flush() {
const queuedCallbacks = [...callbacks.values()]
callbacks.clear()
queuedCallbacks.forEach(callback => callback(performance.now()))
},
}
}
function renderCapacityHarness(options: CapacityHarnessOptions) {
let capacity!: ReturnType<typeof useDashboardMediaGridCapacity>
const Harness = defineComponent({
setup() {
capacity = useDashboardMediaGridCapacity(options)
return {
columnCount: capacity.columnCount,
containerRef: capacity.containerRef,
itemCount: capacity.itemCount,
}
},
template: `
<div ref="containerRef" data-testid="container">
<div class="dashboard-media-content" data-testid="content" />
<span data-testid="columns">{{ columnCount }}</span>
<span data-testid="items">{{ itemCount }}</span>
</div>
`,
})
const rendered = render(Harness)
return { capacity, ...rendered }
}
function setClientWidth(element: HTMLElement, width: number) {
Object.defineProperty(element, 'clientWidth', { configurable: true, value: width })
}
function setBoundingWidth(element: HTMLElement, width: number) {
element.getBoundingClientRect = () =>
({
bottom: 0,
height: 0,
left: 0,
right: width,
toJSON: () => ({}),
top: 0,
width,
x: 0,
y: 0,
}) as DOMRect
}
async function settleInitialMeasurement(frames: ReturnType<typeof createFrameHarness>) {
await nextTick()
frames.flush()
await nextTick()
}
describe('dashboard media grid capacity', () => {
beforeEach(() => {
ResizeObserverMock.instances = []
vi.stubGlobal('ResizeObserver', ResizeObserverMock)
})
it('uses the selected content box and responsive row count to calculate the request size', async () => {
const frames = createFrameHarness()
const rows = ref(2)
const { capacity } = renderCapacityHarness({
contentSelector: '.dashboard-media-content',
maxCount: 10,
minItemWidth: 144,
rows,
})
const content = screen.getByTestId('content')
setClientWidth(content, 760)
content.style.paddingLeft = '20px'
content.style.paddingRight = '20px'
capacity.refreshCapacity()
await nextTick()
expect(screen.getByTestId('columns')).toHaveTextContent('4')
expect(screen.getByTestId('items')).toHaveTextContent('8')
rows.value = 3
await nextTick()
expect(screen.getByTestId('items')).toHaveTextContent('10')
await settleInitialMeasurement(frames)
})
it('falls back to the host width and subtracts the declared horizontal padding', async () => {
const frames = createFrameHarness()
const { capacity } = renderCapacityHarness({ horizontalPadding: 40, minItemWidth: 240 })
const container = screen.getByTestId('container')
setBoundingWidth(container, 760)
capacity.refreshCapacity()
await nextTick()
expect(screen.getByTestId('columns')).toHaveTextContent('2')
expect(screen.getByTestId('items')).toHaveTextContent('4')
await settleInitialMeasurement(frames)
})
it('coalesces observer and window resize notifications into one measurement frame', async () => {
const frames = createFrameHarness()
renderCapacityHarness({ minItemWidth: 200 })
const container = screen.getByTestId('container')
setBoundingWidth(container, 640)
await settleInitialMeasurement(frames)
const observer = ResizeObserverMock.instances.at(-1)
expect(observer?.targets.has(container)).toBe(true)
observer?.trigger()
window.dispatchEvent(new Event('resize'))
expect(frames.callbacks).toHaveLength(1)
frames.flush()
await nextTick()
expect(screen.getByTestId('columns')).toHaveTextContent('3')
expect(screen.getByTestId('items')).toHaveTextContent('6')
})
it('refreshes after KeepAlive activation and releases observers, listeners, and queued frames on unmount', async () => {
const frames = createFrameHarness()
let capacity!: ReturnType<typeof useDashboardMediaGridCapacity>
const Card = defineComponent({
setup() {
capacity = useDashboardMediaGridCapacity({ minItemWidth: 200 })
return { containerRef: capacity.containerRef, itemCount: capacity.itemCount }
},
template: '<div ref="containerRef" data-testid="container">{{ itemCount }}</div>',
})
const Harness = defineComponent({
components: { Card },
setup() {
const active = ref(true)
return { active }
},
template: `
<button type="button" @click="active = false">停用</button>
<button type="button" @click="active = true">启用</button>
<KeepAlive><Card v-if="active" /></KeepAlive>
`,
})
const rendered = render(Harness)
const container = screen.getByTestId('container')
setBoundingWidth(container, 432)
capacity.refreshCapacity()
await nextTick()
expect(container).toHaveTextContent('4')
await fireEvent.click(screen.getByRole('button', { name: '停用' }))
setBoundingWidth(container, 648)
await fireEvent.click(screen.getByRole('button', { name: '启用' }))
await nextTick()
expect(screen.getByTestId('container')).toHaveTextContent('6')
frames.flush()
const observer = ResizeObserverMock.instances.at(-1)
observer?.trigger()
const queuedFrameId = [...frames.callbacks.keys()][0]
rendered.unmount()
expect(observer?.disconnect).toHaveBeenCalledOnce()
expect(frames.cancelFrame).toHaveBeenCalledWith(queuedFrameId)
expect(frames.callbacks).toHaveLength(0)
window.dispatchEvent(new Event('resize'))
expect(frames.callbacks).toHaveLength(0)
})
})
+258 -4
View File
@@ -3,6 +3,7 @@ import { usePluginRuntimeStore } from '@/stores/pluginRuntime'
import { DEFAULT_PERMISSIONS } from '@/utils/permission'
import { renderWithProviders } from '@tests/support/render'
import { fireEvent, screen, waitFor } from '@testing-library/vue'
import { toRaw } from 'vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => {
@@ -220,13 +221,18 @@ function deferred<T>() {
return { promise, resolve }
}
async function renderDashboard() {
async function renderDashboard(
user: {
permissions?: Partial<typeof DEFAULT_PERMISSIONS>
superUser?: boolean
} = {},
) {
return renderWithProviders(DashboardPage, {
initialRoute: '/dashboard',
initialState: {
user: {
permissions: { ...DEFAULT_PERMISSIONS, discovery: true },
superUser: true,
permissions: { ...DEFAULT_PERMISSIONS, discovery: true, ...user.permissions },
superUser: user.superUser ?? true,
},
},
})
@@ -300,11 +306,259 @@ describe('dashboard page initial layout', () => {
mocks.apiGet.mockClear()
const runtimeStore = usePluginRuntimeStore(pinia)
runtimeStore.reconciliation = 1
runtimeStore.reconciliation = 2
await waitFor(() => expect(mocks.apiGet).toHaveBeenCalledWith('/plugin/dashboard/meta'))
})
it('removes a plugin dashboard when runtime metadata no longer advertises it', async () => {
const pluginDashboardId = 'plugin-a:summary'
const enabled = { ...enabledOnlySystemInfo, [pluginDashboardId]: true, systemInfo: false }
localStorage.setItem(
'MP_DASHBOARD_GRID_LAYOUT',
JSON.stringify({ enabled, items: { [pluginDashboardId]: { x: 0, y: 0, w: 6, h: 8 } }, updatedAt: 10 }),
)
localStorage.setItem('MP_DASHBOARD_ORDER', JSON.stringify([{ id: 'plugin-a', key: 'summary' }]))
const latePluginDashboard = deferred<Record<string, unknown>>()
let detailReads = 0
let metaReads = 0
mocks.apiGet.mockImplementation((url: string) => {
if (url === '/user/config/DashboardOrder' || url === '/user/config/DashboardGridLayout') return { data: {} }
if (url === '/plugin/dashboard/meta') {
metaReads += 1
return metaReads <= 2 ? [{ id: 'plugin-a', key: 'summary', name: '插件摘要' }] : []
}
if (url === '/plugin/dashboard/plugin-a/summary') {
detailReads += 1
const dashboard = {
attrs: { refresh: 30 },
cols: { cols: 12 },
elements: [],
id: 'plugin-a',
key: 'summary',
name: '原始名称',
rows: 8,
}
return detailReads === 1 ? dashboard : latePluginDashboard.promise
}
throw new Error('Unexpected GET ' + url)
})
const { pinia } = await renderDashboard()
expect(await screen.findByText('插件摘要')).toBeInTheDocument()
await new Promise(resolve => setTimeout(resolve, 0))
const runtimeStore = usePluginRuntimeStore(pinia)
runtimeStore.reconciliation = 1
await waitFor(() => expect(detailReads).toBe(2))
runtimeStore.reconciliation = 2
await waitFor(() => expect(metaReads).toBe(3))
await waitFor(() => expect(screen.queryByText('插件摘要')).not.toBeInTheDocument())
latePluginDashboard.resolve({
attrs: { refresh: 30 },
cols: { cols: 12 },
elements: [],
id: 'plugin-a',
key: 'summary',
name: '迟到详情',
rows: 8,
})
await new Promise(resolve => setTimeout(resolve, 0))
expect(screen.queryByText('插件摘要')).not.toBeInTheDocument()
})
it('ignores an older plugin metadata response after a newer runtime snapshot is applied', async () => {
const pluginDashboardId = 'plugin-a:summary'
const enabled = { ...enabledOnlySystemInfo, [pluginDashboardId]: true, systemInfo: false }
const staleMeta = deferred<Array<{ id: string; key: string; name: string }>>()
localStorage.setItem(
'MP_DASHBOARD_GRID_LAYOUT',
JSON.stringify({ enabled, items: { [pluginDashboardId]: { x: 0, y: 0, w: 6, h: 8 } }, updatedAt: 10 }),
)
let detailReads = 0
let metaReads = 0
mocks.apiGet.mockImplementation((url: string) => {
if (url === '/user/config/DashboardOrder' || url === '/user/config/DashboardGridLayout') return { data: {} }
if (url === '/plugin/dashboard/meta') {
metaReads += 1
if (metaReads === 1) return [{ id: 'plugin-a', key: 'summary', name: '插件摘要' }]
if (metaReads === 2) return staleMeta.promise
return []
}
if (url === '/plugin/dashboard/plugin-a/summary') {
detailReads += 1
return {
attrs: {},
cols: { cols: 12 },
elements: [],
id: 'plugin-a',
key: 'summary',
name: '原始名称',
rows: 8,
}
}
throw new Error('Unexpected GET ' + url)
})
const { pinia } = await renderDashboard()
expect(await screen.findByText('插件摘要')).toBeInTheDocument()
await new Promise(resolve => setTimeout(resolve, 0))
const runtimeStore = usePluginRuntimeStore(pinia)
runtimeStore.reconciliation = 1
await waitFor(() => expect(metaReads).toBe(2))
runtimeStore.reconciliation = 2
await waitFor(() => expect(metaReads).toBe(3))
await waitFor(() => expect(screen.queryByText('插件摘要')).not.toBeInTheDocument())
staleMeta.resolve([{ id: 'plugin-a', key: 'summary', name: '过期插件摘要' }])
await new Promise(resolve => setTimeout(resolve, 0))
expect(screen.queryByText('过期插件摘要')).not.toBeInTheDocument()
expect(detailReads).toBe(1)
})
it('replaces a plugin refresh timer and clears the active timer on unmount', async () => {
const pluginDashboardId = 'plugin-a:summary'
const enabled = { ...enabledOnlySystemInfo, [pluginDashboardId]: true, systemInfo: false }
localStorage.setItem(
'MP_DASHBOARD_GRID_LAYOUT',
JSON.stringify({ enabled, items: { [pluginDashboardId]: { x: 0, y: 0, w: 6, h: 8 } }, updatedAt: 10 }),
)
const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout')
const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout')
let detailReads = 0
mocks.apiGet.mockImplementation((url: string) => {
if (url === '/user/config/DashboardOrder' || url === '/user/config/DashboardGridLayout') return { data: {} }
if (url === '/plugin/dashboard/meta') return [{ id: 'plugin-a', key: 'summary', name: '插件摘要' }]
if (url === '/plugin/dashboard/plugin-a/summary') {
detailReads += 1
return {
attrs: { refresh: 30 },
cols: { cols: 12 },
elements: [],
id: 'plugin-a',
key: 'summary',
name: '原始名称',
rows: 8,
}
}
throw new Error('Unexpected GET ' + url)
})
const rendered = await renderDashboard()
expect(await screen.findByText('插件摘要')).toBeInTheDocument()
await waitFor(() => expect(setTimeoutSpy.mock.calls.filter(([, delay]) => delay === 30_000)).toHaveLength(1))
const firstTimerCallIndex = setTimeoutSpy.mock.calls.findIndex(([, delay]) => delay === 30_000)
const firstTimer = setTimeoutSpy.mock.results[firstTimerCallIndex].value
const firstTimerCallback = setTimeoutSpy.mock.calls[firstTimerCallIndex][0]
if (typeof firstTimerCallback !== 'function') throw new Error('Expected a plugin refresh callback')
firstTimerCallback()
await waitFor(() => expect(detailReads).toBe(2))
await waitFor(() => expect(setTimeoutSpy.mock.calls.filter(([, delay]) => delay === 30_000)).toHaveLength(2))
const pluginTimerCalls = setTimeoutSpy.mock.calls
.map(([, delay], index) => ({ delay, index }))
.filter(call => call.delay === 30_000)
const secondTimer = setTimeoutSpy.mock.results[pluginTimerCalls[1].index].value
expect(clearTimeoutSpy.mock.calls.some(([timer]) => toRaw(timer) === firstTimer)).toBe(true)
rendered.unmount()
expect(clearTimeoutSpy.mock.calls.some(([timer]) => toRaw(timer) === secondTimer)).toBe(true)
})
it('keeps the accepted plugin refresh chain across transient metadata and detail failures', async () => {
const pluginDashboardId = 'plugin-a:summary'
const enabled = { ...enabledOnlySystemInfo, [pluginDashboardId]: true, systemInfo: false }
localStorage.setItem(
'MP_DASHBOARD_GRID_LAYOUT',
JSON.stringify({ enabled, items: { [pluginDashboardId]: { x: 0, y: 0, w: 6, h: 8 } }, updatedAt: 10 }),
)
const setTimeoutSpy = vi.spyOn(globalThis, 'setTimeout')
vi.spyOn(console, 'error').mockImplementation(() => {})
const staleDetail = deferred<Record<string, unknown>>()
let detailReads = 0
let metaReads = 0
mocks.apiGet.mockImplementation((url: string) => {
if (url === '/user/config/DashboardOrder' || url === '/user/config/DashboardGridLayout') return { data: {} }
if (url === '/plugin/dashboard/meta') {
metaReads += 1
if (metaReads === 2) return Promise.reject(new Error('temporary metadata failure'))
return [{ id: 'plugin-a', key: 'summary', name: '插件摘要' }]
}
if (url === '/plugin/dashboard/plugin-a/summary') {
detailReads += 1
if (detailReads === 3) return staleDetail.promise
if (detailReads === 4) return Promise.reject(new Error('temporary detail failure'))
return {
attrs: { refresh: 30 },
cols: { cols: 12 },
elements: [],
id: 'plugin-a',
key: 'summary',
name: '原始名称',
rows: 8,
}
}
throw new Error('Unexpected GET ' + url)
})
const { pinia } = await renderDashboard()
expect(await screen.findByText('插件摘要')).toBeInTheDocument()
await new Promise(resolve => setTimeout(resolve, 0))
const runtimeStore = usePluginRuntimeStore(pinia)
const pluginTimerCallbacks = () =>
setTimeoutSpy.mock.calls.filter(([, delay]) => delay === 30_000).map(([callback]) => callback as () => void)
runtimeStore.reconciliation += 1
await waitFor(() => expect(metaReads).toBe(2))
pluginTimerCallbacks()[0]()
await waitFor(() => expect(detailReads).toBe(2))
await waitFor(() => expect(pluginTimerCallbacks()).toHaveLength(2))
pluginTimerCallbacks()[1]()
await waitFor(() => expect(detailReads).toBe(3))
runtimeStore.reconciliation += 1
await waitFor(() => expect(detailReads).toBe(4))
staleDetail.resolve({
attrs: { refresh: 30 },
cols: { cols: 12 },
elements: [],
id: 'plugin-a',
key: 'summary',
name: '过期详情',
rows: 8,
})
await waitFor(() => expect(pluginTimerCallbacks()).toHaveLength(3))
})
it('hides discovery content and dashboard controls without their permissions', async () => {
mocks.apiGet.mockImplementation((url: string) => {
if (
url === '/user/config/DashboardOrder' ||
url === '/user/config/DashboardGridLayout' ||
url === '/user/config/Dashboard'
) {
return { data: {} }
}
if (url === '/plugin/dashboard/meta') return []
throw new Error('Unexpected GET ' + url)
})
const { container } = await renderDashboard({
permissions: { admin: false, discovery: false },
superUser: false,
})
expect(container.querySelector('[data-dashboard-id="mediaRecommend"]')).not.toBeInTheDocument()
expect(document.querySelector('.compact-fab-stack')).not.toBeInTheDocument()
})
it('disables automatic grid transitions only while browsing with the glass theme', async () => {
mocks.themeName.value = 'glass'
mocks.apiGet.mockImplementation((url: string) => {
+67 -13
View File
@@ -97,6 +97,15 @@ interface DashboardGridItem {
widget: GridStackWidget
}
interface PluginDashboardMetaItem {
/** 插件稳定 ID。 */
id: string
/** 多仪表板入口键;空值表示插件唯一仪表板。 */
key?: string
/** 插件声明的仪表板显示名称。 */
name?: string
}
// 是否处于仪表板布局编辑模式
const isLayoutEditing = ref(false)
@@ -309,11 +318,15 @@ const dashboardConfigs = ref<DashboardItem[]>([
])
// 插件的仪表板元信息
const pluginDashboardMeta = ref<any[]>([])
const pluginDashboardMeta = ref<PluginDashboardMetaItem[]>([])
// 插件仪表板的刷新状态
const pluginDashboardRefreshStatus = ref<{ [key: string]: boolean }>({})
// 请求序号只负责丢弃乱序响应;已接受代际用于约束详情响应不得跨运行态恢复旧卡片。
let latestPluginDashboardMetaRequestId = 0
let acceptedPluginDashboardMetaGeneration = 0
// 当前启用且可渲染的仪表板 Grid 项。
const dashboardGridItems = computed<DashboardGridItem[]>(() =>
dashboardConfigs.value
@@ -1091,23 +1104,49 @@ async function saveDashboardConfig(payload?: { enabled?: Record<string, boolean>
}
// 构造插件仪表板主ID
function buildPluginDashboardId(plugin_id: string, key: string) {
function buildPluginDashboardId(plugin_id: string, key?: string) {
if (!key) return plugin_id
return plugin_id + ':' + key
}
// 调用API获取所有插件的仪表板元信息
async function getPluginDashboardMeta() {
const requestId = ++latestPluginDashboardMetaRequestId
try {
pluginDashboardMeta.value = (await api.get('/plugin/dashboard/meta')) ?? []
const nextPluginDashboardMeta: PluginDashboardMetaItem[] = (await api.get('/plugin/dashboard/meta')) ?? []
if (requestId !== latestPluginDashboardMetaRequestId) return
const metaGeneration = ++acceptedPluginDashboardMetaGeneration
const nextPluginDashboardIds = new Set(
nextPluginDashboardMeta.map(item => buildPluginDashboardId(item.id, item.key)),
)
const removedPluginDashboardIds = pluginDashboardMeta.value
.map(item => buildPluginDashboardId(item.id, item.key))
.filter(id => !nextPluginDashboardIds.has(id))
pluginDashboardMeta.value = nextPluginDashboardMeta
if (removedPluginDashboardIds.length > 0) {
const removedIdSet = new Set(removedPluginDashboardIds)
removedPluginDashboardIds.forEach(pluginDashboardId => {
clearPluginDashboardTimer(pluginDashboardId)
delete pluginDashboardRefreshStatus.value[pluginDashboardId]
})
// 运行态集合不再包含这些插件时移除渲染配置;用户保存的启用与布局偏好继续保留。
dashboardConfigs.value = dashboardConfigs.value.filter(
item => !removedIdSet.has(buildPluginDashboardId(item.id, item.key)),
)
}
if (!isNullOrEmptyObject(pluginDashboardMeta.value)) {
// 下载插件仪表板配置
await Promise.all(
pluginDashboardMeta.value.map(async (pluginDashboard: { id: string; key: string }) => {
pluginDashboardMeta.value.map(async pluginDashboard => {
const pluginDashboardId = buildPluginDashboardId(pluginDashboard.id, pluginDashboard.key)
// 初始化插件仪表板的刷新状态
pluginDashboardRefreshStatus.value[pluginDashboardId] = true
await getPluginDashboard(pluginDashboard.id, pluginDashboard.key)
await getPluginDashboard(pluginDashboard.id, pluginDashboard.key ?? '', metaGeneration)
}),
)
}
@@ -1136,34 +1175,46 @@ function schedulePluginDashboardRefresh(item: DashboardItem) {
isRequest.value
) {
refreshTimers.value[pluginDashboardId] = setTimeout(() => {
void getPluginDashboard(item.id, item.key)
void getPluginDashboard(item.id, item.key, acceptedPluginDashboardMetaGeneration)
}, item.attrs.refresh * 1000)
}
}
// 并发刷新失败或过期时,沿用当前已渲染配置保证仍有且仅有一个后续刷新任务。
function ensurePluginDashboardRefresh(id: string, key: string) {
const isCurrentPluginDashboard = pluginDashboardMeta.value.some(item => item.id === id && (item.key ?? '') === key)
if (!isCurrentPluginDashboard) return
const currentDashboard = dashboardConfigs.value.find(item => item.id === id && item.key === key)
if (currentDashboard) schedulePluginDashboardRefresh(currentDashboard)
}
// 重新拉取当前启用的插件仪表板数据。
function refreshEnabledPluginDashboards() {
if (isNullOrEmptyObject(pluginDashboardMeta.value)) return
pluginDashboardMeta.value.forEach((pluginDashboard: { id: string; key: string }) => {
pluginDashboardMeta.value.forEach(pluginDashboard => {
const pluginDashboardId = buildPluginDashboardId(pluginDashboard.id, pluginDashboard.key)
if (enableConfig.value[pluginDashboardId]) {
void getPluginDashboard(pluginDashboard.id, pluginDashboard.key)
void getPluginDashboard(pluginDashboard.id, pluginDashboard.key ?? '', acceptedPluginDashboardMetaGeneration)
}
})
}
// 获取一个插件的仪表板配置项
async function getPluginDashboard(id: string, key: string) {
async function getPluginDashboard(id: string, key: string, metaGeneration: number) {
let refreshScheduled = false
try {
const url = key ? `/plugin/dashboard/${id}/${key}` : `/plugin/dashboard/${id}`
const res: DashboardItem | undefined = await api.get(url)
if (res) {
if (metaGeneration !== acceptedPluginDashboardMetaGeneration) return
// 名称替换为元信息的名称
const meta = pluginDashboardMeta.value.find(
(item: { id: string; key: string }) => item.id === id && item.key === key,
)
if (meta) res.name = meta.name
const meta = pluginDashboardMeta.value.find(item => item.id === id && (item.key ?? '') === key)
if (!meta) return
if (meta.name) res.name = meta.name
// 保存到仪表板配置中,如果已经存在则替换
const index = dashboardConfigs.value.findIndex(
(item: { id: string; key: string }) => item.id === id && item.key === key,
@@ -1177,9 +1228,12 @@ async function getPluginDashboard(id: string, key: string) {
}
// 定时刷新
schedulePluginDashboardRefresh(res)
refreshScheduled = true
}
} catch (error) {
console.error(error)
} finally {
if (!refreshScheduled) ensurePluginDashboardRefresh(id, key)
}
}
+7
View File
@@ -343,6 +343,7 @@ export default defineConfig(({ command, mode, isPreview }) => ({
'src/views/subscribe/SubscribeShareView.vue',
'src/composables/useMediaSubscribe.ts',
'src/composables/useLlmProviderDirectory.ts',
'src/composables/useDashboardMediaGridCapacity.ts',
'src/composables/useOfflineStatus.ts',
'src/composables/useScheduleProgress.ts',
'src/composables/useServerConnectionProbe.ts',
@@ -436,6 +437,12 @@ export default defineConfig(({ command, mode, isPreview }) => ({
lines: 90,
statements: 90,
},
'src/composables/useDashboardMediaGridCapacity.ts': {
branches: 85,
functions: 90,
lines: 90,
statements: 90,
},
'src/composables/useOfflineStatus.ts': {
branches: 85,
functions: 90,