|
|
|
@@ -1,7 +1,8 @@
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { ReloadOutlined } from '@ant-design/icons-vue'
|
|
|
|
|
import { EyeOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
|
|
|
|
import type {
|
|
|
|
|
DesktopAccountInfo,
|
|
|
|
|
MonitoringCitedArticle,
|
|
|
|
|
MonitoringHotQuestion,
|
|
|
|
|
MonitoringPlatformAuthorizationStatus,
|
|
|
|
|
} from '@geo/shared-types'
|
|
|
|
@@ -14,6 +15,7 @@ import { computed, ref, watch } from 'vue'
|
|
|
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
|
|
|
|
|
|
|
import ArticleDetailDrawer from '@/components/ArticleDetailDrawer.vue'
|
|
|
|
|
import PageHero from '@/components/PageHero.vue'
|
|
|
|
|
import { brandsApi, monitoringApi, tenantAccountsApi } from '@/lib/api'
|
|
|
|
|
import { resolveAccountCheckedAt, resolveAccountHealth } from '@/lib/desktop-account-runtime'
|
|
|
|
@@ -34,6 +36,7 @@ const companyStore = useCompanyStore()
|
|
|
|
|
const trackingMaxHistoryDays = 30
|
|
|
|
|
const trackingBusinessTimeZone = 'Asia/Shanghai'
|
|
|
|
|
const trackingToday = formatTrackingBusinessDate(new Date())
|
|
|
|
|
const trackingListPageSize = 10
|
|
|
|
|
|
|
|
|
|
type CitationRankingRow = {
|
|
|
|
|
ai_platform_id: string
|
|
|
|
@@ -105,6 +108,11 @@ const selectedBusinessDate = ref<string>(
|
|
|
|
|
normalizeTrackingBusinessDate(route.query.business_date) ?? trackingToday,
|
|
|
|
|
)
|
|
|
|
|
const selectedCitationWindowDays = useStorage<number>('tracking_selected_citation_window_days', 7)
|
|
|
|
|
const hotQuestionSearch = ref('')
|
|
|
|
|
const hotQuestionPage = ref(1)
|
|
|
|
|
const citedArticlePage = ref(1)
|
|
|
|
|
const articleDrawerOpen = ref(false)
|
|
|
|
|
const selectedArticleId = ref<number | null>(null)
|
|
|
|
|
|
|
|
|
|
selectedPlatformId.value = normalizeMonitoringPlatformFilter(selectedPlatformId.value)
|
|
|
|
|
selectedCitationWindowDays.value = normalizeCitationWindowDays(selectedCitationWindowDays.value)
|
|
|
|
@@ -239,6 +247,8 @@ const dashboardQuery = useQuery({
|
|
|
|
|
selectedQuestionId.value,
|
|
|
|
|
selectedPlatformId.value,
|
|
|
|
|
selectedBusinessDate.value,
|
|
|
|
|
hotQuestionPage.value,
|
|
|
|
|
hotQuestionSearch.value.trim(),
|
|
|
|
|
]),
|
|
|
|
|
enabled: computed(() => Boolean(selectedBrandId.value)),
|
|
|
|
|
queryFn: () =>
|
|
|
|
@@ -248,6 +258,9 @@ const dashboardQuery = useQuery({
|
|
|
|
|
days: trackingMaxHistoryDays,
|
|
|
|
|
ai_platform_id: selectedPlatformId.value !== 'all' ? selectedPlatformId.value : undefined,
|
|
|
|
|
business_date: selectedBusinessDate.value,
|
|
|
|
|
hot_questions_page: hotQuestionPage.value,
|
|
|
|
|
hot_questions_page_size: trackingListPageSize,
|
|
|
|
|
hot_questions_q: hotQuestionSearch.value.trim() || undefined,
|
|
|
|
|
}),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
@@ -260,6 +273,7 @@ const citationSummaryQuery = useQuery({
|
|
|
|
|
selectedPlatformId.value,
|
|
|
|
|
selectedBusinessDate.value,
|
|
|
|
|
selectedCitationWindowDays.value,
|
|
|
|
|
citedArticlePage.value,
|
|
|
|
|
]),
|
|
|
|
|
enabled: computed(() => Boolean(selectedBrandId.value)),
|
|
|
|
|
queryFn: () =>
|
|
|
|
@@ -269,6 +283,8 @@ const citationSummaryQuery = useQuery({
|
|
|
|
|
business_date: selectedBusinessDate.value,
|
|
|
|
|
ai_platform_id: selectedPlatformId.value !== 'all' ? selectedPlatformId.value : undefined,
|
|
|
|
|
days: selectedCitationWindowDays.value,
|
|
|
|
|
cited_articles_page: citedArticlePage.value,
|
|
|
|
|
cited_articles_page_size: trackingListPageSize,
|
|
|
|
|
}),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
@@ -375,6 +391,12 @@ const hotQuestions = computed(() => {
|
|
|
|
|
return dashboardQuery.data.value?.hot_questions ?? []
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const hotQuestionsPageInfo = computed(() => {
|
|
|
|
|
return dashboardQuery.data.value?.hot_questions_page
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const hotQuestionsTotal = computed(() => hotQuestionsPageInfo.value?.total ?? 0)
|
|
|
|
|
|
|
|
|
|
const aiPlatformStatusCards = computed<AIPlatformStatusCard[]>(() => {
|
|
|
|
|
const accountGroups = new Map<string, DesktopAccountInfo[]>()
|
|
|
|
|
for (const account of aiAccountsQuery.data.value ?? []) {
|
|
|
|
@@ -541,10 +563,13 @@ const citationRankingRows = computed<CitationRankingRow[]>(() => {
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const visibleCitedArticles = computed(() => {
|
|
|
|
|
const citedArticles = computed(() => {
|
|
|
|
|
return citationSummaryQuery.data.value?.cited_articles ?? []
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const citedArticlesPageInfo = computed(() => citationSummaryQuery.data.value?.cited_articles_page)
|
|
|
|
|
const citedArticlesTotal = computed(() => citedArticlesPageInfo.value?.total ?? 0)
|
|
|
|
|
|
|
|
|
|
const citationPieSlices = computed(() => {
|
|
|
|
|
const segments = citationRankingRows.value.filter(
|
|
|
|
|
(item) => (item.saas_source_count ?? item.cited_answer_count ?? 0) > 0,
|
|
|
|
@@ -624,6 +649,18 @@ const heroDescription = computed(() => {
|
|
|
|
|
return `最近一次采集数据:${formatDateTime(overview.last_sampled_at)}`
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
watch(hotQuestionSearch, () => {
|
|
|
|
|
hotQuestionPage.value = 1
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
watch([selectedBrandId, selectedQuestionId, selectedPlatformId, selectedBusinessDate], () => {
|
|
|
|
|
hotQuestionPage.value = 1
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
watch([selectedBrandId, selectedQuestionId, selectedPlatformId, selectedBusinessDate, selectedCitationWindowDays], () => {
|
|
|
|
|
citedArticlePage.value = 1
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function openQuestion(question: MonitoringHotQuestion): void {
|
|
|
|
|
if (!selectedBrandId.value) {
|
|
|
|
|
return
|
|
|
|
@@ -646,6 +683,11 @@ function openQuestion(question: MonitoringHotQuestion): void {
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openCitedArticle(item: MonitoringCitedArticle): void {
|
|
|
|
|
selectedArticleId.value = item.article_id
|
|
|
|
|
articleDrawerOpen.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filterQuestionOption(input: string, option?: { label?: string }): boolean {
|
|
|
|
|
const keyword = input.trim().toLowerCase()
|
|
|
|
|
if (!keyword) {
|
|
|
|
@@ -1192,9 +1234,17 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
|
|
|
|
</a-card>
|
|
|
|
|
|
|
|
|
|
<a-card class="tracking-panel" :loading="dashboardQuery.isLoading.value">
|
|
|
|
|
<div class="tracking-panel__header">
|
|
|
|
|
<h3 class="panel-title">{{ t('tracking.hotQuestionsTitle') }}</h3>
|
|
|
|
|
<span class="tracking-panel__helper">{{ t('tracking.hotQuestionsHint') }}</span>
|
|
|
|
|
<div class="tracking-panel__header tracking-panel__header--list">
|
|
|
|
|
<div class="tracking-panel__header-left">
|
|
|
|
|
<h3 class="panel-title">{{ t('tracking.hotQuestionsTitle') }}</h3>
|
|
|
|
|
<span class="tracking-panel__helper">{{ t('tracking.hotQuestionsHint') }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<a-input-search
|
|
|
|
|
v-model:value="hotQuestionSearch"
|
|
|
|
|
allow-clear
|
|
|
|
|
class="tracking-list-search"
|
|
|
|
|
:placeholder="t('tracking.questionSearchPlaceholder')"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="hotQuestions.length" class="question-list">
|
|
|
|
@@ -1217,7 +1267,24 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<a-empty v-else :description="t('tracking.noQuestions')" />
|
|
|
|
|
<a-empty
|
|
|
|
|
v-else
|
|
|
|
|
:description="
|
|
|
|
|
hotQuestionSearch.trim()
|
|
|
|
|
? t('tracking.noQuestionSearchResults')
|
|
|
|
|
: t('tracking.noQuestions')
|
|
|
|
|
"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<div v-if="hotQuestionsTotal > trackingListPageSize" class="tracking-pagination">
|
|
|
|
|
<a-pagination
|
|
|
|
|
v-model:current="hotQuestionPage"
|
|
|
|
|
:page-size="trackingListPageSize"
|
|
|
|
|
:total="hotQuestionsTotal"
|
|
|
|
|
:show-size-changer="false"
|
|
|
|
|
size="small"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</a-card>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
@@ -1230,29 +1297,58 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="article-list">
|
|
|
|
|
<div v-if="citedArticles.length" class="article-list">
|
|
|
|
|
<div
|
|
|
|
|
v-for="item in visibleCitedArticles"
|
|
|
|
|
v-for="item in citedArticles"
|
|
|
|
|
:key="item.article_id"
|
|
|
|
|
class="article-list__item"
|
|
|
|
|
>
|
|
|
|
|
<div>
|
|
|
|
|
<div class="article-list__content">
|
|
|
|
|
<strong>{{ item.article_title }}</strong>
|
|
|
|
|
<p>{{ item.publish_platform }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="article-list__meta">
|
|
|
|
|
<span>{{ item.citation_count }} {{ t('tracking.citations') }}</span>
|
|
|
|
|
<span>{{ t('tracking.columns.share') }} {{ formatPercent(item.source_share) }}</span>
|
|
|
|
|
<strong>{{ formatPercent(item.citation_rate) }}</strong>
|
|
|
|
|
<div class="article-list__side">
|
|
|
|
|
<div class="article-list__meta">
|
|
|
|
|
<span>{{ item.citation_count }} {{ t('tracking.citations') }}</span>
|
|
|
|
|
<span>
|
|
|
|
|
{{ t('tracking.columns.share') }} {{ formatPercent(item.source_share) }}
|
|
|
|
|
</span>
|
|
|
|
|
<strong>{{ formatPercent(item.citation_rate) }}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
<a-tooltip :title="t('tracking.viewArticleContent')">
|
|
|
|
|
<a-button
|
|
|
|
|
type="text"
|
|
|
|
|
shape="circle"
|
|
|
|
|
class="article-list__preview-button"
|
|
|
|
|
:aria-label="t('tracking.viewArticleContent')"
|
|
|
|
|
@click="openCitedArticle(item)"
|
|
|
|
|
>
|
|
|
|
|
<template #icon><EyeOutlined /></template>
|
|
|
|
|
</a-button>
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<a-empty
|
|
|
|
|
v-if="!visibleCitedArticles.length"
|
|
|
|
|
:description="t('tracking.noCitedArticles')"
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<a-empty v-else :description="t('tracking.noCitedArticles')" />
|
|
|
|
|
|
|
|
|
|
<div v-if="citedArticlesTotal > trackingListPageSize" class="tracking-pagination">
|
|
|
|
|
<a-pagination
|
|
|
|
|
v-model:current="citedArticlePage"
|
|
|
|
|
:page-size="trackingListPageSize"
|
|
|
|
|
:total="citedArticlesTotal"
|
|
|
|
|
:show-size-changer="false"
|
|
|
|
|
size="small"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</a-card>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<ArticleDetailDrawer
|
|
|
|
|
:open="articleDrawerOpen"
|
|
|
|
|
:article-id="selectedArticleId"
|
|
|
|
|
@close="articleDrawerOpen = false"
|
|
|
|
|
/>
|
|
|
|
|
</section>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
@@ -1456,6 +1552,10 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tracking-panel__header--list {
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.panel-title {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-size: 16px;
|
|
|
|
@@ -1486,6 +1586,11 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tracking-list-search {
|
|
|
|
|
width: min(100%, 260px);
|
|
|
|
|
flex: 0 0 260px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tracking-panel__empty {
|
|
|
|
|
padding: 28px 0 16px;
|
|
|
|
|
}
|
|
|
|
@@ -1831,6 +1936,37 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: flex-end;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
min-width: 84px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.article-list__content {
|
|
|
|
|
min-width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.article-list__content strong {
|
|
|
|
|
overflow-wrap: anywhere;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.article-list__side {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.article-list__preview-button {
|
|
|
|
|
color: #4b5563;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.article-list__preview-button:hover {
|
|
|
|
|
color: #1f5cff;
|
|
|
|
|
background: #eef4ff;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tracking-pagination {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-tabs {
|
|
|
|
@@ -1911,6 +2047,30 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 640px) {
|
|
|
|
|
.tracking-panel__header--list {
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tracking-list-search {
|
|
|
|
|
width: 100%;
|
|
|
|
|
flex-basis: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-list__item,
|
|
|
|
|
.article-list__item {
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.question-list__time {
|
|
|
|
|
align-self: flex-end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.article-list__side {
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tracking-citation-header {
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|