perf(admin-web): reduce redundant polling and cover active publish status
Frontend CI / Frontend (push) Successful in 3m35s
Backend CI / Backend (push) Failing after 9m46s

Unify list/detail poll intervals to 5s and skip refetch while a query is
already in flight, replacing broad invalidateQueries cascades with targeted
refetches. Poll now also triggers on active publish status via the new
hasActivePublishStatus helper. Dedupe concurrent refreshBrands calls with a
shared promise and skip the redundant AppShell refresh when already
initialized. Gate publish-record loading on popover open.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-08 23:16:47 +08:00
parent 2c11910b6c
commit 5d29703265
12 changed files with 167 additions and 54 deletions
+44 -2
View File
@@ -5,7 +5,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import type { TableColumnsType } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import type { Dayjs } from 'dayjs'
import { computed, reactive, ref } from 'vue'
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
@@ -18,7 +18,7 @@ import { articlesApi } from '@/lib/api'
import { buildArticleClipboardContent, resolveArticleActionState } from '@/lib/article-list-actions'
import { useArticleRegenerateAction } from '@/lib/article-regenerate-action'
import { copyTextToClipboard } from '@/lib/clipboard'
import { getPublishStatusMeta } from '@/lib/display'
import { getPublishStatusMeta, hasActivePublishStatus } from '@/lib/display'
import { formatError } from '@/lib/errors'
import { useCompanyStore } from '@/stores/company'
@@ -34,6 +34,8 @@ const selectedArticleId = ref<number | null>(null)
const page = ref(1)
const pageSize = ref(10)
const createdRange = ref<[Dayjs, Dayjs] | null>(null)
let articlePollingTimer: number | null = null
const ARTICLE_LIST_POLL_INTERVAL_MS = 5000
const draftFilters = reactive<{
publish_status?: string
@@ -172,11 +174,51 @@ function openPreview(article: ArticleListItem): void {
articleDrawerOpen.value = true
}
watch(
() => articleListQuery.data.value?.items || [],
(items: ArticleListItem[]) => {
const hasPublishingArticles = items.some((item) => hasActivePublishStatus(item.publish_status))
if (hasPublishingArticles) {
startArticlePolling()
return
}
stopArticlePolling()
},
{ immediate: true },
)
onBeforeUnmount(() => {
stopArticlePolling()
})
function handleTableChange(nextPage: number, nextPageSize: number): void {
page.value = nextPage
pageSize.value = nextPageSize
}
function startArticlePolling(): void {
if (articlePollingTimer !== null) {
return
}
articlePollingTimer = window.setInterval(() => {
if (!articleListQuery.isFetching.value) {
void articleListQuery.refetch()
}
}, ARTICLE_LIST_POLL_INTERVAL_MS)
}
function stopArticlePolling(): void {
if (articlePollingTimer === null) {
return
}
window.clearInterval(articlePollingTimer)
articlePollingTimer = null
}
async function handleDelete(articleId: number): Promise<void> {
await deleteMutation.mutateAsync(articleId)
}
+8 -4
View File
@@ -23,6 +23,7 @@ import {
getGenerateStatusMeta,
getPublishStatusMeta,
hasActiveGenerationStatus,
hasActivePublishStatus,
} from '@/lib/display'
import { formatError } from '@/lib/errors'
import { useCompanyStore } from '@/stores/company'
@@ -93,6 +94,7 @@ const articleListQuery = useQuery({
})
let articlePollingTimer: number | null = null
const ARTICLE_LIST_POLL_INTERVAL_MS = 5000
const deleteMutation = useMutation({
mutationFn: (articleId: number) => articlesApi.remove(articleId),
@@ -180,8 +182,9 @@ watch(
const hasGeneratingArticles = items.some((item) =>
hasActiveGenerationStatus(item.generate_status),
)
const hasPublishingArticles = items.some((item) => hasActivePublishStatus(item.publish_status))
if (hasGeneratingArticles) {
if (hasGeneratingArticles || hasPublishingArticles) {
startArticlePolling()
return
}
@@ -245,9 +248,10 @@ function startArticlePolling(): void {
}
articlePollingTimer = window.setInterval(() => {
void queryClient.invalidateQueries({ queryKey: ['articles'] })
void articleListQuery.refetch()
}, 3000)
if (!articleListQuery.isFetching.value) {
void articleListQuery.refetch()
}
}, ARTICLE_LIST_POLL_INTERVAL_MS)
}
function stopArticlePolling(): void {
+17 -5
View File
@@ -42,6 +42,7 @@ import {
getPublishStatusMeta,
getTemplateMeta,
hasActiveGenerationStatus,
hasActivePublishStatus,
} from '@/lib/display'
import { formatError } from '@/lib/errors'
import { useCompanyStore } from '@/stores/company'
@@ -207,6 +208,7 @@ const displayedArticleTotal = computed(() =>
)
let articlePollingTimer: number | null = null
const ARTICLE_LIST_POLL_INTERVAL_MS = 5000
const deleteMutation = useMutation({
mutationFn: (articleId: number) => articlesApi.remove(articleId),
@@ -418,8 +420,11 @@ watch(
const hasGeneratingArticles = items.some((item: ArticleListItem) =>
hasActiveGenerationStatus(item.generate_status),
)
const hasPublishingArticles = items.some((item: ArticleListItem) =>
hasActivePublishStatus(item.publish_status),
)
if (hasGeneratingArticles) {
if (hasGeneratingArticles || hasPublishingArticles) {
startArticlePolling()
return
}
@@ -444,10 +449,17 @@ function startArticlePolling(): void {
}
articlePollingTimer = window.setInterval(() => {
void queryClient.invalidateQueries({ queryKey: ['articles'] })
void queryClient.invalidateQueries({ queryKey: ['workspace', 'recent-articles'] })
void Promise.all([articleListQuery.refetch(), recentArticlesQuery.refetch()])
}, 3000)
if (shouldUseRecentFallback.value) {
if (!recentArticlesQuery.isFetching.value) {
void recentArticlesQuery.refetch()
}
return
}
if (!articleListQuery.isFetching.value) {
void articleListQuery.refetch()
}
}, ARTICLE_LIST_POLL_INTERVAL_MS)
}
function stopArticlePolling(): void {
+13 -7
View File
@@ -31,7 +31,12 @@ import { articlesApi, tenantAccountsApi, workspaceApi } from '@/lib/api'
import { buildArticleClipboardContent, resolveArticleActionState } from '@/lib/article-list-actions'
import { useArticleRegenerateAction } from '@/lib/article-regenerate-action'
import { copyTextToClipboard } from '@/lib/clipboard'
import { formatDateTime, getTemplateMeta, hasActiveGenerationStatus } from '@/lib/display'
import {
formatDateTime,
getTemplateMeta,
hasActiveGenerationStatus,
hasActivePublishStatus,
} from '@/lib/display'
import { formatError } from '@/lib/errors'
import { isMediaPublishAccount } from '@/lib/publish-platforms'
import { useCompanyStore } from '@/stores/company'
@@ -45,6 +50,7 @@ const articleDrawerOpen = ref(false)
const selectedPublishArticleId = ref<number | null>(null)
const publishModalOpen = ref(false)
let recentArticlesPollingTimer: number | null = null
const ARTICLE_LIST_POLL_INTERVAL_MS = 5000
const overviewQuery = useQuery({
queryKey: computed(() => ['workspace', 'overview', companyStore.currentBrandId]),
@@ -300,8 +306,9 @@ watch(
const hasGeneratingArticles = items.some((item) =>
hasActiveGenerationStatus(item.generate_status),
)
const hasPublishingArticles = items.some((item) => hasActivePublishStatus(item.publish_status))
if (hasGeneratingArticles) {
if (hasGeneratingArticles || hasPublishingArticles) {
startRecentArticlesPolling()
return
}
@@ -325,12 +332,11 @@ function startRecentArticlesPolling(): void {
stopRecentArticlesPolling()
return
}
void queryClient.invalidateQueries({
queryKey: ['workspace', 'recent-articles', companyStore.currentBrandId],
})
void queryClient.invalidateQueries({ queryKey: ['articles'] })
if (recentArticlesQuery.isFetching.value) {
return
}
void recentArticlesQuery.refetch()
}, 3000)
}, ARTICLE_LIST_POLL_INTERVAL_MS)
}
function stopRecentArticlesPolling(): void {