feat(questions): make brand questions the primary monitoring & template axis
- add /questions/combination-fill endpoint with AI-driven, IP-region-aware matrix fill - extract ip2region resolver from ops/app into shared/ipregion for cross-service use - thread question_id filter through dashboard composite, citation summary, and collect-now - switch template wizard from keyword inputs to brand-question selection (primary + supplemental) - pass brand_question and supplemental_questions through assist/title/outline prompts - add AiWaitingModal + 45s client timeout and request_timeout error mapping for long AI flows Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -96,9 +96,10 @@ const trackingPlatformOptions = [
|
||||
value: platform.id,
|
||||
})),
|
||||
] as const
|
||||
const allQuestionOptionValue = 'all'
|
||||
|
||||
const selectedBrandId = useStorage<number | null>('tracking_selected_brand', null)
|
||||
const selectedKeywordId = useStorage<number | null>('tracking_selected_keyword', null)
|
||||
const selectedQuestionId = useStorage<number | null>('tracking_selected_question', null)
|
||||
const selectedPlatformId = useStorage<string>('tracking_selected_ai_platform_id', 'all')
|
||||
const selectedBusinessDate = useStorage<string>('tracking_selected_business_date', trackingToday)
|
||||
const selectedCitationWindowDays = useStorage<number>('tracking_selected_citation_window_days', 7)
|
||||
@@ -108,6 +109,14 @@ selectedBusinessDate.value =
|
||||
normalizeTrackingBusinessDate(selectedBusinessDate.value) ?? trackingToday
|
||||
selectedCitationWindowDays.value = normalizeCitationWindowDays(selectedCitationWindowDays.value)
|
||||
|
||||
const selectedQuestionSetOptionValue = computed<string | number>({
|
||||
get: () => selectedQuestionId.value ?? allQuestionOptionValue,
|
||||
set: (value) => {
|
||||
selectedQuestionId.value =
|
||||
typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null
|
||||
},
|
||||
})
|
||||
|
||||
const brandsQuery = useQuery({
|
||||
queryKey: ['tracking', 'brands'],
|
||||
queryFn: () => brandsApi.list(),
|
||||
@@ -135,37 +144,48 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const keywordsQuery = useQuery({
|
||||
queryKey: computed(() => ['tracking', 'keywords', selectedBrandId.value]),
|
||||
const questionsQuery = useQuery({
|
||||
queryKey: computed(() => ['tracking', 'questions', selectedBrandId.value]),
|
||||
enabled: computed(() => Boolean(selectedBrandId.value)),
|
||||
queryFn: () => brandsApi.listKeywords(selectedBrandId.value as number),
|
||||
queryFn: () => brandsApi.listQuestions(selectedBrandId.value as number),
|
||||
})
|
||||
|
||||
const questionSetOptions = computed(() => [
|
||||
{ label: t('tracking.allQuestionSets'), value: allQuestionOptionValue },
|
||||
...(questionsQuery.data.value ?? []).map((item) => ({
|
||||
label: item.question_text,
|
||||
value: item.id,
|
||||
})),
|
||||
])
|
||||
|
||||
watch(
|
||||
() => keywordsQuery.data.value,
|
||||
(keywords) => {
|
||||
if (!keywords?.length) {
|
||||
() => questionsQuery.data.value,
|
||||
(questions) => {
|
||||
const requestedQuestionId = parseNumericQuery(route.query.question_id)
|
||||
if (!questions?.length) {
|
||||
if (!requestedQuestionId) {
|
||||
selectedQuestionId.value = null
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const requestedKeywordId = parseNumericQuery(route.query.keyword_id)
|
||||
if (requestedKeywordId && keywords.some((item) => item.id === requestedKeywordId)) {
|
||||
selectedKeywordId.value = requestedKeywordId
|
||||
if (requestedQuestionId && questions.some((item) => item.id === requestedQuestionId)) {
|
||||
selectedQuestionId.value = requestedQuestionId
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedKeywordId.value && keywords.some((item) => item.id === selectedKeywordId.value)) {
|
||||
if (selectedQuestionId.value && questions.some((item) => item.id === selectedQuestionId.value)) {
|
||||
return
|
||||
}
|
||||
|
||||
selectedKeywordId.value = keywords[0].id
|
||||
selectedQuestionId.value = null
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(selectedBrandId, (newId, oldId) => {
|
||||
if (oldId && newId !== oldId) {
|
||||
selectedKeywordId.value = null
|
||||
selectedQuestionId.value = null
|
||||
}
|
||||
})
|
||||
|
||||
@@ -189,14 +209,25 @@ watch(
|
||||
)
|
||||
|
||||
watch(
|
||||
[selectedBrandId, selectedKeywordId, selectedPlatformId, selectedBusinessDate],
|
||||
([brandId, keywordId, platformId, businessDate]) => {
|
||||
[
|
||||
selectedBrandId,
|
||||
selectedQuestionId,
|
||||
selectedPlatformId,
|
||||
selectedBusinessDate,
|
||||
() => questionsQuery.data.value,
|
||||
],
|
||||
([brandId, questionId, platformId, businessDate, questions]) => {
|
||||
const requestedQuestionId = parseNumericQuery(route.query.question_id)
|
||||
if (requestedQuestionId && !questionId && !Array.isArray(questions)) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextBrandId = brandId ? String(brandId) : undefined
|
||||
const nextKeywordId = keywordId ? String(keywordId) : undefined
|
||||
const nextQuestionId = questionId ? String(questionId) : undefined
|
||||
const nextPlatformId = platformId !== 'all' ? platformId : undefined
|
||||
const nextBusinessDate = normalizeTrackingBusinessDate(businessDate) ?? trackingToday
|
||||
const currentBrandId = normalizeQueryValue(route.query.brand_id) || undefined
|
||||
const currentKeywordId = normalizeQueryValue(route.query.keyword_id) || undefined
|
||||
const currentQuestionId = normalizeQueryValue(route.query.question_id) || undefined
|
||||
const currentPlatformId = normalizeMonitoringPlatformFilter(route.query.ai_platform_id)
|
||||
const currentBusinessDate =
|
||||
normalizeTrackingBusinessDate(route.query.business_date) ?? trackingToday
|
||||
@@ -204,7 +235,7 @@ watch(
|
||||
|
||||
if (
|
||||
currentBrandId === nextBrandId &&
|
||||
currentKeywordId === nextKeywordId &&
|
||||
currentQuestionId === nextQuestionId &&
|
||||
currentPlatformId === (nextPlatformId ?? 'all') &&
|
||||
currentBusinessDate === nextBusinessDate &&
|
||||
!hasLegacyCitationDaysQuery
|
||||
@@ -216,7 +247,7 @@ watch(
|
||||
name: 'tracking',
|
||||
query: {
|
||||
...(nextBrandId ? { brand_id: nextBrandId } : {}),
|
||||
...(nextKeywordId ? { keyword_id: nextKeywordId } : {}),
|
||||
...(nextQuestionId ? { question_id: nextQuestionId } : {}),
|
||||
...(nextPlatformId ? { ai_platform_id: nextPlatformId } : {}),
|
||||
business_date: nextBusinessDate,
|
||||
},
|
||||
@@ -229,7 +260,7 @@ const dashboardQuery = useQuery({
|
||||
'tracking',
|
||||
'dashboard',
|
||||
selectedBrandId.value,
|
||||
selectedKeywordId.value,
|
||||
selectedQuestionId.value,
|
||||
selectedPlatformId.value,
|
||||
selectedBusinessDate.value,
|
||||
]),
|
||||
@@ -237,7 +268,7 @@ const dashboardQuery = useQuery({
|
||||
queryFn: () =>
|
||||
monitoringApi.dashboardComposite({
|
||||
brand_id: selectedBrandId.value ?? undefined,
|
||||
keyword_id: selectedKeywordId.value,
|
||||
question_id: selectedQuestionId.value,
|
||||
days: trackingMaxHistoryDays,
|
||||
ai_platform_id: selectedPlatformId.value !== 'all' ? selectedPlatformId.value : undefined,
|
||||
business_date: selectedBusinessDate.value,
|
||||
@@ -249,7 +280,7 @@ const citationSummaryQuery = useQuery({
|
||||
'tracking',
|
||||
'citation-summary',
|
||||
selectedBrandId.value,
|
||||
selectedKeywordId.value,
|
||||
selectedQuestionId.value,
|
||||
selectedPlatformId.value,
|
||||
selectedBusinessDate.value,
|
||||
selectedCitationWindowDays.value,
|
||||
@@ -258,7 +289,7 @@ const citationSummaryQuery = useQuery({
|
||||
queryFn: () =>
|
||||
monitoringApi.citationSummary({
|
||||
brand_id: selectedBrandId.value ?? undefined,
|
||||
keyword_id: selectedKeywordId.value,
|
||||
question_id: selectedQuestionId.value,
|
||||
business_date: selectedBusinessDate.value,
|
||||
ai_platform_id: selectedPlatformId.value !== 'all' ? selectedPlatformId.value : undefined,
|
||||
days: selectedCitationWindowDays.value,
|
||||
@@ -275,12 +306,9 @@ const collectNowMutation = useMutation({
|
||||
if (!selectedBrandId.value) {
|
||||
throw new Error('tracking_brand_required')
|
||||
}
|
||||
if (!selectedKeywordId.value) {
|
||||
throw new Error('tracking_keyword_required')
|
||||
}
|
||||
|
||||
return monitoringApi.collectNow(selectedBrandId.value, {
|
||||
keyword_id: selectedKeywordId.value,
|
||||
question_id: selectedQuestionId.value ?? undefined,
|
||||
platform_ids: selectedPlatformId.value !== 'all' ? [selectedPlatformId.value] : undefined,
|
||||
})
|
||||
},
|
||||
@@ -293,10 +321,6 @@ const collectNowMutation = useMutation({
|
||||
message.warning(t('tracking.collectBrandRequired'))
|
||||
return
|
||||
}
|
||||
if (error instanceof Error && error.message === 'tracking_keyword_required') {
|
||||
message.warning(t('tracking.collectKeywordRequired'))
|
||||
return
|
||||
}
|
||||
message.error(formatError(error))
|
||||
},
|
||||
})
|
||||
@@ -343,9 +367,6 @@ const collectNowDisabledReason = computed(() => {
|
||||
if (!selectedBrandId.value) {
|
||||
return t('tracking.collectBrandRequired')
|
||||
}
|
||||
if (!selectedKeywordId.value) {
|
||||
return t('tracking.collectKeywordRequired')
|
||||
}
|
||||
if (selectedBusinessDate.value !== trackingToday) {
|
||||
return t('tracking.collectTodayOnly')
|
||||
}
|
||||
@@ -649,11 +670,20 @@ function openQuestion(question: MonitoringHotQuestion): void {
|
||||
date_from: selectedBusinessDate.value,
|
||||
date_to: selectedBusinessDate.value,
|
||||
...(selectedPlatformId.value !== 'all' ? { ai_platform_id: selectedPlatformId.value } : {}),
|
||||
...(selectedKeywordId.value ? { keyword_id: String(selectedKeywordId.value) } : {}),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function filterQuestionOption(input: string, option?: { label?: string }): boolean {
|
||||
const keyword = input.trim().toLowerCase()
|
||||
if (!keyword) {
|
||||
return true
|
||||
}
|
||||
return String(option?.label ?? '')
|
||||
.toLowerCase()
|
||||
.includes(keyword)
|
||||
}
|
||||
|
||||
function formatPercent(value: number | null | undefined): string {
|
||||
if (value === null || value === undefined) {
|
||||
return '--'
|
||||
@@ -832,18 +862,16 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
||||
/>
|
||||
</div>
|
||||
<div class="tracking-action-item">
|
||||
<span class="tracking-action-label">关键词</span>
|
||||
<span class="tracking-action-label">问题集</span>
|
||||
<a-select
|
||||
v-model:value="selectedKeywordId"
|
||||
allow-clear
|
||||
v-model:value="selectedQuestionSetOptionValue"
|
||||
show-search
|
||||
class="tracking-select"
|
||||
option-filter-prop="label"
|
||||
:filter-option="filterQuestionOption"
|
||||
:not-found-content="t('tracking.noQuestions')"
|
||||
:placeholder="t('tracking.keywordPlaceholder')"
|
||||
:options="
|
||||
(keywordsQuery.data.value ?? []).map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}))
|
||||
"
|
||||
:options="questionSetOptions"
|
||||
/>
|
||||
</div>
|
||||
<div class="tracking-action-item">
|
||||
|
||||
Reference in New Issue
Block a user