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
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { CopyOutlined, LinkOutlined } from '@ant-design/icons-vue'
import type { ArticleVersion, PublishRecord } from '@geo/shared-types'
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { useQuery } from '@tanstack/vue-query'
import type { TableColumnsType } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { computed, ref, watch } from 'vue'
@@ -22,6 +22,7 @@ import {
getPublishStatusMeta,
getSourceTypeLabel,
hasActiveGenerationStatus,
hasActivePublishStatus,
} from '@/lib/display'
import { useCompanyStore } from '@/stores/company'
@@ -34,7 +35,6 @@ const emit = defineEmits<{
close: []
}>()
const queryClient = useQueryClient()
const { t } = useI18n()
const companyStore = useCompanyStore()
const activeTab = ref('content')
@@ -43,6 +43,7 @@ const streamTitle = ref('')
const streamMarkdown = ref('')
const streamStatus = ref<string | null>(null)
const streamFeatureEnabled = isGenerationStreamEnabled()
const ARTICLE_DETAIL_POLL_INTERVAL_MS = 5000
const detailQuery = useQuery({
queryKey: computed(() => ['articles', 'detail', companyStore.currentBrandId, props.articleId]),
@@ -178,18 +179,30 @@ watch(
)
watch(
[() => props.open, () => props.articleId, () => detail.value?.generate_status],
([open, articleId, generateStatus], _prev, onCleanup) => {
if (!open || !articleId || !hasActiveGenerationStatus(generateStatus)) {
[
() => props.open,
() => props.articleId,
() => detail.value?.generate_status,
() => detail.value?.publish_status,
],
([open, articleId, generateStatus, publishStatus], _prev, onCleanup) => {
const shouldPoll =
hasActiveGenerationStatus(generateStatus) || hasActivePublishStatus(publishStatus)
if (!open || !articleId || !shouldPoll) {
return
}
const timer = window.setInterval(
() => {
void refreshArticleData()
},
streamFeatureEnabled ? 5000 : 2000,
)
const timer = window.setInterval(() => {
if (
detailQuery.isFetching.value ||
versionsQuery.isFetching.value ||
publishRecordsQuery.isFetching.value
) {
return
}
void refreshArticleData()
}, ARTICLE_DETAIL_POLL_INTERVAL_MS)
onCleanup(() => {
window.clearInterval(timer)
@@ -234,13 +247,7 @@ function resetStreamState(): void {
}
async function refreshArticleData(): Promise<void> {
await Promise.all([
detailQuery.refetch(),
versionsQuery.refetch(),
publishRecordsQuery.refetch(),
queryClient.invalidateQueries({ queryKey: ['articles'] }),
queryClient.invalidateQueries({ queryKey: ['workspace'] }),
])
await Promise.all([detailQuery.refetch(), versionsQuery.refetch(), publishRecordsQuery.refetch()])
}
function resolvePublishLink(record: PublishRecord): string {
@@ -63,7 +63,9 @@ const publishRecordsQuery = useQuery({
companyStore.currentBrandId,
props.articleId,
]),
enabled: computed(() => shouldLoadRecords.value && Boolean(companyStore.currentBrandId)),
enabled: computed(
() => popoverOpen.value && shouldLoadRecords.value && Boolean(companyStore.currentBrandId),
),
queryFn: () => articlesApi.publishRecords(props.articleId as number),
staleTime: 30 * 1000,
})
@@ -17,7 +17,12 @@ import { articlesApi, promptRulesApi } from '@/lib/api'
import { buildArticleClipboardContent, resolveArticleActionState } from '@/lib/article-list-actions'
import { useArticleRegenerateAction } from '@/lib/article-regenerate-action'
import { copyTextToClipboard } from '@/lib/clipboard'
import { getGenerateStatusMeta, getPublishStatusMeta } from '@/lib/display'
import {
getGenerateStatusMeta,
getPublishStatusMeta,
hasActiveGenerationStatus,
hasActivePublishStatus,
} from '@/lib/display'
import { formatError } from '@/lib/errors'
import { formatPublishPlatformList } from '@/lib/publish-platforms'
import { useCompanyStore } from '@/stores/company'
@@ -109,6 +114,7 @@ const listQuery = useQuery({
})
let pollingTimer: number | null = null
const ARTICLE_LIST_POLL_INTERVAL_MS = 5000
const deleteMutation = useMutation({
mutationFn: (id: number) => articlesApi.remove(id),
@@ -205,8 +211,10 @@ function handleTableChange(nextPage: number, nextPageSize: number): void {
function startPolling(): void {
if (pollingTimer !== null) return
pollingTimer = window.setInterval(() => {
void listQuery.refetch()
}, 3000)
if (!listQuery.isFetching.value) {
void listQuery.refetch()
}
}, ARTICLE_LIST_POLL_INTERVAL_MS)
}
function stopPolling(): void {
@@ -236,7 +244,9 @@ watch(
() => listQuery.data.value?.items ?? [],
(items) => {
const hasActive = items.some(
(item) => item.generate_status === 'generating' || item.generate_status === 'running',
(item) =>
hasActiveGenerationStatus(item.generate_status) ||
hasActivePublishStatus(item.publish_status),
)
if (hasActive) {
startPolling()
@@ -117,6 +117,7 @@ const columns = computed<TableColumnsType<InstantTaskItem>>(() => [
])
let pollingTimer: number | null = null
const TASK_LIST_POLL_INTERVAL_MS = 5000
function applyFilters(): void {
page.value = 1
@@ -153,8 +154,10 @@ function openTaskArticles(task: InstantTaskItem): void {
function startPolling(): void {
if (pollingTimer !== null) return
pollingTimer = window.setInterval(() => {
void listQuery.refetch()
}, 3000)
if (!listQuery.isFetching.value) {
void listQuery.refetch()
}
}, TASK_LIST_POLL_INTERVAL_MS)
}
function stopPolling(): void {
@@ -235,12 +235,15 @@ function openTaskArticles(task: ScheduleTask): void {
}
let pollingTimer: number | null = null
const TASK_LIST_POLL_INTERVAL_MS = 5000
function startPolling(): void {
if (pollingTimer !== null) return
pollingTimer = window.setInterval(() => {
void listQuery.refetch()
}, 3000)
if (!listQuery.isFetching.value) {
void listQuery.refetch()
}
}, TASK_LIST_POLL_INTERVAL_MS)
}
function stopPolling(): void {