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"> <script setup lang="ts">
import { CopyOutlined, LinkOutlined } from '@ant-design/icons-vue' import { CopyOutlined, LinkOutlined } from '@ant-design/icons-vue'
import type { ArticleVersion, PublishRecord } from '@geo/shared-types' 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 type { TableColumnsType } from 'ant-design-vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
@@ -22,6 +22,7 @@ import {
getPublishStatusMeta, getPublishStatusMeta,
getSourceTypeLabel, getSourceTypeLabel,
hasActiveGenerationStatus, hasActiveGenerationStatus,
hasActivePublishStatus,
} from '@/lib/display' } from '@/lib/display'
import { useCompanyStore } from '@/stores/company' import { useCompanyStore } from '@/stores/company'
@@ -34,7 +35,6 @@ const emit = defineEmits<{
close: [] close: []
}>() }>()
const queryClient = useQueryClient()
const { t } = useI18n() const { t } = useI18n()
const companyStore = useCompanyStore() const companyStore = useCompanyStore()
const activeTab = ref('content') const activeTab = ref('content')
@@ -43,6 +43,7 @@ const streamTitle = ref('')
const streamMarkdown = ref('') const streamMarkdown = ref('')
const streamStatus = ref<string | null>(null) const streamStatus = ref<string | null>(null)
const streamFeatureEnabled = isGenerationStreamEnabled() const streamFeatureEnabled = isGenerationStreamEnabled()
const ARTICLE_DETAIL_POLL_INTERVAL_MS = 5000
const detailQuery = useQuery({ const detailQuery = useQuery({
queryKey: computed(() => ['articles', 'detail', companyStore.currentBrandId, props.articleId]), queryKey: computed(() => ['articles', 'detail', companyStore.currentBrandId, props.articleId]),
@@ -178,18 +179,30 @@ watch(
) )
watch( watch(
[() => props.open, () => props.articleId, () => detail.value?.generate_status], [
([open, articleId, generateStatus], _prev, onCleanup) => { () => props.open,
if (!open || !articleId || !hasActiveGenerationStatus(generateStatus)) { () => 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 return
} }
const timer = window.setInterval( const timer = window.setInterval(() => {
() => { if (
void refreshArticleData() detailQuery.isFetching.value ||
}, versionsQuery.isFetching.value ||
streamFeatureEnabled ? 5000 : 2000, publishRecordsQuery.isFetching.value
) ) {
return
}
void refreshArticleData()
}, ARTICLE_DETAIL_POLL_INTERVAL_MS)
onCleanup(() => { onCleanup(() => {
window.clearInterval(timer) window.clearInterval(timer)
@@ -234,13 +247,7 @@ function resetStreamState(): void {
} }
async function refreshArticleData(): Promise<void> { async function refreshArticleData(): Promise<void> {
await Promise.all([ await Promise.all([detailQuery.refetch(), versionsQuery.refetch(), publishRecordsQuery.refetch()])
detailQuery.refetch(),
versionsQuery.refetch(),
publishRecordsQuery.refetch(),
queryClient.invalidateQueries({ queryKey: ['articles'] }),
queryClient.invalidateQueries({ queryKey: ['workspace'] }),
])
} }
function resolvePublishLink(record: PublishRecord): string { function resolvePublishLink(record: PublishRecord): string {
@@ -63,7 +63,9 @@ const publishRecordsQuery = useQuery({
companyStore.currentBrandId, companyStore.currentBrandId,
props.articleId, 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), queryFn: () => articlesApi.publishRecords(props.articleId as number),
staleTime: 30 * 1000, staleTime: 30 * 1000,
}) })
@@ -17,7 +17,12 @@ import { articlesApi, promptRulesApi } from '@/lib/api'
import { buildArticleClipboardContent, resolveArticleActionState } from '@/lib/article-list-actions' import { buildArticleClipboardContent, resolveArticleActionState } from '@/lib/article-list-actions'
import { useArticleRegenerateAction } from '@/lib/article-regenerate-action' import { useArticleRegenerateAction } from '@/lib/article-regenerate-action'
import { copyTextToClipboard } from '@/lib/clipboard' 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 { formatError } from '@/lib/errors'
import { formatPublishPlatformList } from '@/lib/publish-platforms' import { formatPublishPlatformList } from '@/lib/publish-platforms'
import { useCompanyStore } from '@/stores/company' import { useCompanyStore } from '@/stores/company'
@@ -109,6 +114,7 @@ const listQuery = useQuery({
}) })
let pollingTimer: number | null = null let pollingTimer: number | null = null
const ARTICLE_LIST_POLL_INTERVAL_MS = 5000
const deleteMutation = useMutation({ const deleteMutation = useMutation({
mutationFn: (id: number) => articlesApi.remove(id), mutationFn: (id: number) => articlesApi.remove(id),
@@ -205,8 +211,10 @@ function handleTableChange(nextPage: number, nextPageSize: number): void {
function startPolling(): void { function startPolling(): void {
if (pollingTimer !== null) return if (pollingTimer !== null) return
pollingTimer = window.setInterval(() => { pollingTimer = window.setInterval(() => {
void listQuery.refetch() if (!listQuery.isFetching.value) {
}, 3000) void listQuery.refetch()
}
}, ARTICLE_LIST_POLL_INTERVAL_MS)
} }
function stopPolling(): void { function stopPolling(): void {
@@ -236,7 +244,9 @@ watch(
() => listQuery.data.value?.items ?? [], () => listQuery.data.value?.items ?? [],
(items) => { (items) => {
const hasActive = items.some( const hasActive = items.some(
(item) => item.generate_status === 'generating' || item.generate_status === 'running', (item) =>
hasActiveGenerationStatus(item.generate_status) ||
hasActivePublishStatus(item.publish_status),
) )
if (hasActive) { if (hasActive) {
startPolling() startPolling()
@@ -117,6 +117,7 @@ const columns = computed<TableColumnsType<InstantTaskItem>>(() => [
]) ])
let pollingTimer: number | null = null let pollingTimer: number | null = null
const TASK_LIST_POLL_INTERVAL_MS = 5000
function applyFilters(): void { function applyFilters(): void {
page.value = 1 page.value = 1
@@ -153,8 +154,10 @@ function openTaskArticles(task: InstantTaskItem): void {
function startPolling(): void { function startPolling(): void {
if (pollingTimer !== null) return if (pollingTimer !== null) return
pollingTimer = window.setInterval(() => { pollingTimer = window.setInterval(() => {
void listQuery.refetch() if (!listQuery.isFetching.value) {
}, 3000) void listQuery.refetch()
}
}, TASK_LIST_POLL_INTERVAL_MS)
} }
function stopPolling(): void { function stopPolling(): void {
@@ -235,12 +235,15 @@ function openTaskArticles(task: ScheduleTask): void {
} }
let pollingTimer: number | null = null let pollingTimer: number | null = null
const TASK_LIST_POLL_INTERVAL_MS = 5000
function startPolling(): void { function startPolling(): void {
if (pollingTimer !== null) return if (pollingTimer !== null) return
pollingTimer = window.setInterval(() => { pollingTimer = window.setInterval(() => {
void listQuery.refetch() if (!listQuery.isFetching.value) {
}, 3000) void listQuery.refetch()
}
}, TASK_LIST_POLL_INTERVAL_MS)
} }
function stopPolling(): void { function stopPolling(): void {
+1 -1
View File
@@ -518,7 +518,7 @@ onMounted(() => {
syncMobileState(mobileMediaQuery) syncMobileState(mobileMediaQuery)
mobileMediaQuery.addEventListener('change', syncMobileState) mobileMediaQuery.addEventListener('change', syncMobileState)
if (!isMembershipBlocked.value) { if (!isMembershipBlocked.value && !companyStore.initialized) {
void companyStore.refreshBrands() void companyStore.refreshBrands()
} }
}) })
+12
View File
@@ -89,6 +89,18 @@ export function hasActiveGenerationStatus(status?: string | null): boolean {
) )
} }
export function hasActivePublishStatus(status?: string | null): boolean {
const normalized = String(status ?? '')
.trim()
.toLowerCase()
return (
normalized === 'queued' ||
normalized === 'pending' ||
normalized === 'publishing' ||
normalized === 'running'
)
}
export function getPublishStatusMeta(status?: string | null): { label: string; color: string } { export function getPublishStatusMeta(status?: string | null): { label: string; color: string } {
if (!status) { if (!status) {
return { label: '--', color: 'default' } return { label: '--', color: 'default' }
+20 -8
View File
@@ -15,6 +15,7 @@ export const useCompanyStore = defineStore('company', () => {
const currentBrandId = ref<number | null>(readStoredCurrentBrandId()) const currentBrandId = ref<number | null>(readStoredCurrentBrandId())
const loading = ref(false) const loading = ref(false)
const initialized = ref(false) const initialized = ref(false)
let refreshPromise: Promise<Brand[]> | null = null
const currentBrand = computed( const currentBrand = computed(
() => brands.value.find((brand) => brand.id === currentBrandId.value) ?? null, () => brands.value.find((brand) => brand.id === currentBrandId.value) ?? null,
@@ -56,15 +57,25 @@ export const useCompanyStore = defineStore('company', () => {
} }
async function refreshBrands(): Promise<Brand[]> { async function refreshBrands(): Promise<Brand[]> {
loading.value = true if (refreshPromise) {
try { return refreshPromise
const nextBrands = normalizeBrandList(await brandsApi.list())
setBrands(nextBrands)
initialized.value = true
return nextBrands
} finally {
loading.value = false
} }
loading.value = true
refreshPromise = brandsApi
.list()
.then((value) => {
const nextBrands = normalizeBrandList(value)
setBrands(nextBrands)
initialized.value = true
return nextBrands
})
.finally(() => {
loading.value = false
refreshPromise = null
})
return refreshPromise
} }
function setBrands(value: Brand[] | null | undefined): void { function setBrands(value: Brand[] | null | undefined): void {
@@ -78,6 +89,7 @@ export const useCompanyStore = defineStore('company', () => {
currentBrandId.value = null currentBrandId.value = null
initialized.value = false initialized.value = false
loading.value = false loading.value = false
refreshPromise = null
clearStoredCurrentBrandId() clearStoredCurrentBrandId()
} }
+44 -2
View File
@@ -5,7 +5,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import type { TableColumnsType } from 'ant-design-vue' import type { TableColumnsType } from 'ant-design-vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import type { Dayjs } from 'dayjs' 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 { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -18,7 +18,7 @@ import { articlesApi } from '@/lib/api'
import { buildArticleClipboardContent, resolveArticleActionState } from '@/lib/article-list-actions' import { buildArticleClipboardContent, resolveArticleActionState } from '@/lib/article-list-actions'
import { useArticleRegenerateAction } from '@/lib/article-regenerate-action' import { useArticleRegenerateAction } from '@/lib/article-regenerate-action'
import { copyTextToClipboard } from '@/lib/clipboard' import { copyTextToClipboard } from '@/lib/clipboard'
import { getPublishStatusMeta } from '@/lib/display' import { getPublishStatusMeta, hasActivePublishStatus } from '@/lib/display'
import { formatError } from '@/lib/errors' import { formatError } from '@/lib/errors'
import { useCompanyStore } from '@/stores/company' import { useCompanyStore } from '@/stores/company'
@@ -34,6 +34,8 @@ const selectedArticleId = ref<number | null>(null)
const page = ref(1) const page = ref(1)
const pageSize = ref(10) const pageSize = ref(10)
const createdRange = ref<[Dayjs, Dayjs] | null>(null) const createdRange = ref<[Dayjs, Dayjs] | null>(null)
let articlePollingTimer: number | null = null
const ARTICLE_LIST_POLL_INTERVAL_MS = 5000
const draftFilters = reactive<{ const draftFilters = reactive<{
publish_status?: string publish_status?: string
@@ -172,11 +174,51 @@ function openPreview(article: ArticleListItem): void {
articleDrawerOpen.value = true 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 { function handleTableChange(nextPage: number, nextPageSize: number): void {
page.value = nextPage page.value = nextPage
pageSize.value = nextPageSize 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> { async function handleDelete(articleId: number): Promise<void> {
await deleteMutation.mutateAsync(articleId) await deleteMutation.mutateAsync(articleId)
} }
+8 -4
View File
@@ -23,6 +23,7 @@ import {
getGenerateStatusMeta, getGenerateStatusMeta,
getPublishStatusMeta, getPublishStatusMeta,
hasActiveGenerationStatus, hasActiveGenerationStatus,
hasActivePublishStatus,
} from '@/lib/display' } from '@/lib/display'
import { formatError } from '@/lib/errors' import { formatError } from '@/lib/errors'
import { useCompanyStore } from '@/stores/company' import { useCompanyStore } from '@/stores/company'
@@ -93,6 +94,7 @@ const articleListQuery = useQuery({
}) })
let articlePollingTimer: number | null = null let articlePollingTimer: number | null = null
const ARTICLE_LIST_POLL_INTERVAL_MS = 5000
const deleteMutation = useMutation({ const deleteMutation = useMutation({
mutationFn: (articleId: number) => articlesApi.remove(articleId), mutationFn: (articleId: number) => articlesApi.remove(articleId),
@@ -180,8 +182,9 @@ watch(
const hasGeneratingArticles = items.some((item) => const hasGeneratingArticles = items.some((item) =>
hasActiveGenerationStatus(item.generate_status), hasActiveGenerationStatus(item.generate_status),
) )
const hasPublishingArticles = items.some((item) => hasActivePublishStatus(item.publish_status))
if (hasGeneratingArticles) { if (hasGeneratingArticles || hasPublishingArticles) {
startArticlePolling() startArticlePolling()
return return
} }
@@ -245,9 +248,10 @@ function startArticlePolling(): void {
} }
articlePollingTimer = window.setInterval(() => { articlePollingTimer = window.setInterval(() => {
void queryClient.invalidateQueries({ queryKey: ['articles'] }) if (!articleListQuery.isFetching.value) {
void articleListQuery.refetch() void articleListQuery.refetch()
}, 3000) }
}, ARTICLE_LIST_POLL_INTERVAL_MS)
} }
function stopArticlePolling(): void { function stopArticlePolling(): void {
+17 -5
View File
@@ -42,6 +42,7 @@ import {
getPublishStatusMeta, getPublishStatusMeta,
getTemplateMeta, getTemplateMeta,
hasActiveGenerationStatus, hasActiveGenerationStatus,
hasActivePublishStatus,
} from '@/lib/display' } from '@/lib/display'
import { formatError } from '@/lib/errors' import { formatError } from '@/lib/errors'
import { useCompanyStore } from '@/stores/company' import { useCompanyStore } from '@/stores/company'
@@ -207,6 +208,7 @@ const displayedArticleTotal = computed(() =>
) )
let articlePollingTimer: number | null = null let articlePollingTimer: number | null = null
const ARTICLE_LIST_POLL_INTERVAL_MS = 5000
const deleteMutation = useMutation({ const deleteMutation = useMutation({
mutationFn: (articleId: number) => articlesApi.remove(articleId), mutationFn: (articleId: number) => articlesApi.remove(articleId),
@@ -418,8 +420,11 @@ watch(
const hasGeneratingArticles = items.some((item: ArticleListItem) => const hasGeneratingArticles = items.some((item: ArticleListItem) =>
hasActiveGenerationStatus(item.generate_status), hasActiveGenerationStatus(item.generate_status),
) )
const hasPublishingArticles = items.some((item: ArticleListItem) =>
hasActivePublishStatus(item.publish_status),
)
if (hasGeneratingArticles) { if (hasGeneratingArticles || hasPublishingArticles) {
startArticlePolling() startArticlePolling()
return return
} }
@@ -444,10 +449,17 @@ function startArticlePolling(): void {
} }
articlePollingTimer = window.setInterval(() => { articlePollingTimer = window.setInterval(() => {
void queryClient.invalidateQueries({ queryKey: ['articles'] }) if (shouldUseRecentFallback.value) {
void queryClient.invalidateQueries({ queryKey: ['workspace', 'recent-articles'] }) if (!recentArticlesQuery.isFetching.value) {
void Promise.all([articleListQuery.refetch(), recentArticlesQuery.refetch()]) void recentArticlesQuery.refetch()
}, 3000) }
return
}
if (!articleListQuery.isFetching.value) {
void articleListQuery.refetch()
}
}, ARTICLE_LIST_POLL_INTERVAL_MS)
} }
function stopArticlePolling(): void { 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 { buildArticleClipboardContent, resolveArticleActionState } from '@/lib/article-list-actions'
import { useArticleRegenerateAction } from '@/lib/article-regenerate-action' import { useArticleRegenerateAction } from '@/lib/article-regenerate-action'
import { copyTextToClipboard } from '@/lib/clipboard' 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 { formatError } from '@/lib/errors'
import { isMediaPublishAccount } from '@/lib/publish-platforms' import { isMediaPublishAccount } from '@/lib/publish-platforms'
import { useCompanyStore } from '@/stores/company' import { useCompanyStore } from '@/stores/company'
@@ -45,6 +50,7 @@ const articleDrawerOpen = ref(false)
const selectedPublishArticleId = ref<number | null>(null) const selectedPublishArticleId = ref<number | null>(null)
const publishModalOpen = ref(false) const publishModalOpen = ref(false)
let recentArticlesPollingTimer: number | null = null let recentArticlesPollingTimer: number | null = null
const ARTICLE_LIST_POLL_INTERVAL_MS = 5000
const overviewQuery = useQuery({ const overviewQuery = useQuery({
queryKey: computed(() => ['workspace', 'overview', companyStore.currentBrandId]), queryKey: computed(() => ['workspace', 'overview', companyStore.currentBrandId]),
@@ -300,8 +306,9 @@ watch(
const hasGeneratingArticles = items.some((item) => const hasGeneratingArticles = items.some((item) =>
hasActiveGenerationStatus(item.generate_status), hasActiveGenerationStatus(item.generate_status),
) )
const hasPublishingArticles = items.some((item) => hasActivePublishStatus(item.publish_status))
if (hasGeneratingArticles) { if (hasGeneratingArticles || hasPublishingArticles) {
startRecentArticlesPolling() startRecentArticlesPolling()
return return
} }
@@ -325,12 +332,11 @@ function startRecentArticlesPolling(): void {
stopRecentArticlesPolling() stopRecentArticlesPolling()
return return
} }
void queryClient.invalidateQueries({ if (recentArticlesQuery.isFetching.value) {
queryKey: ['workspace', 'recent-articles', companyStore.currentBrandId], return
}) }
void queryClient.invalidateQueries({ queryKey: ['articles'] })
void recentArticlesQuery.refetch() void recentArticlesQuery.refetch()
}, 3000) }, ARTICLE_LIST_POLL_INTERVAL_MS)
} }
function stopRecentArticlesPolling(): void { function stopRecentArticlesPolling(): void {