feat: paginate and search brand questions across selects
Make GET /api/tenant/brands/:id/questions return a paginated QuestionListResponse (items/total/page/page_size) with optional `q` full-text filter, validated page/page_size query params, and per-params cache keys. Add a usePaginatedBrandQuestions composable backing the imitation, template-wizard, and tracking question selects with search-as-you-type and infinite scroll, plus ensure-loaded-by-id so a deep-linked or pre-selected question is fetched even when off the first page. Brands view now drives its questions table via server pagination. Also redesign the Tracking hot-questions and cited-articles lists (mention-rate badges/bars, metric groups, refreshed styling) and fall back to citation-fact article_id/title when no high-confidence URL alias matches, so cited articles surface even without an alias row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,9 @@ const competitorForm = reactive({
|
||||
product_lines: '',
|
||||
})
|
||||
|
||||
const questionPage = ref(1)
|
||||
const questionPageSize = ref(10)
|
||||
|
||||
const brandListQuery = useQuery({
|
||||
queryKey: ['brands', 'list'],
|
||||
queryFn: () => brandsApi.list(),
|
||||
@@ -74,9 +77,19 @@ const brandLibrarySummaryQuery = useQuery({
|
||||
})
|
||||
|
||||
const questionsQuery = useQuery({
|
||||
queryKey: computed(() => ['brands', selectedBrandId.value, 'questions', 'all']),
|
||||
queryKey: computed(() => [
|
||||
'brands',
|
||||
selectedBrandId.value,
|
||||
'questions',
|
||||
questionPage.value,
|
||||
questionPageSize.value,
|
||||
]),
|
||||
enabled: computed(() => Boolean(selectedBrandId.value)),
|
||||
queryFn: () => brandsApi.listQuestions(selectedBrandId.value as number),
|
||||
queryFn: () =>
|
||||
brandsApi.listQuestions(selectedBrandId.value as number, {
|
||||
page: questionPage.value,
|
||||
page_size: questionPageSize.value,
|
||||
}),
|
||||
})
|
||||
|
||||
const competitorsQuery = useQuery({
|
||||
@@ -93,7 +106,8 @@ const selectedBrand = computed(
|
||||
() => brandListQuery.data.value?.find((item) => item.id === selectedBrandId.value) ?? null,
|
||||
)
|
||||
|
||||
const currentQuestions = computed(() => questionsQuery.data.value ?? [])
|
||||
const currentQuestions = computed(() => questionsQuery.data.value?.items ?? [])
|
||||
const questionTotal = computed(() => questionsQuery.data.value?.total ?? 0)
|
||||
|
||||
const maxQuestions = computed(
|
||||
() =>
|
||||
@@ -102,7 +116,7 @@ const maxQuestions = computed(
|
||||
0,
|
||||
)
|
||||
const usedQuestions = computed(
|
||||
() => brandLibrarySummary.value?.used_questions ?? currentQuestions.value.length,
|
||||
() => brandLibrarySummary.value?.used_questions ?? questionTotal.value,
|
||||
)
|
||||
|
||||
const brandLimitReached = computed(() => {
|
||||
@@ -308,6 +322,13 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch([questionTotal, questionPageSize], ([total, pageSize]) => {
|
||||
const maxPage = Math.max(1, Math.ceil(total / pageSize))
|
||||
if (questionPage.value > maxPage) {
|
||||
questionPage.value = maxPage
|
||||
}
|
||||
})
|
||||
|
||||
function parseProductLines(value: string): string[] | undefined {
|
||||
const lines = value
|
||||
.split(',')
|
||||
@@ -344,6 +365,7 @@ function selectBrand(brandId: number): void {
|
||||
return
|
||||
}
|
||||
selectedBrandId.value = brandId
|
||||
questionPage.value = 1
|
||||
void queryClient.invalidateQueries({ queryKey: ['brands'] })
|
||||
}
|
||||
|
||||
@@ -429,6 +451,11 @@ async function submitSingleQuestion(): Promise<void> {
|
||||
await questionMutations.createSingle.mutateAsync()
|
||||
}
|
||||
|
||||
function handleQuestionTableChange(nextPage: number, nextPageSize: number): void {
|
||||
questionPage.value = nextPage
|
||||
questionPageSize.value = nextPageSize
|
||||
}
|
||||
|
||||
async function submitCompetitor(): Promise<void> {
|
||||
if (!competitorForm.name.trim()) {
|
||||
return
|
||||
@@ -551,7 +578,13 @@ async function invalidateBrandQueries(): Promise<void> {
|
||||
:columns="questionColumns"
|
||||
:data-source="currentQuestions"
|
||||
:loading="questionsQuery.isPending.value"
|
||||
:pagination="{ pageSize: 10, showSizeChanger: false }"
|
||||
:pagination="{
|
||||
current: questionPage,
|
||||
pageSize: questionPageSize,
|
||||
total: questionTotal,
|
||||
showSizeChanger: false,
|
||||
onChange: handleQuestionTableChange,
|
||||
}"
|
||||
row-key="id"
|
||||
>
|
||||
<template #emptyText>{{ t('brands.empty.questions') }}</template>
|
||||
|
||||
Reference in New Issue
Block a user