feat: add monitoring marked articles and retention updates
This commit is contained in:
@@ -0,0 +1,472 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
EyeOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import type { MonitoringMarkedArticle, MonitoringMarkedArticleRequest } from '@geo/shared-types'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import type { TableColumnsType, TablePaginationConfig } from 'ant-design-vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import PageHero from '@/components/PageHero.vue'
|
||||
import { monitoringApi } from '@/lib/api'
|
||||
import { formatDateTime } from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { useCompanyStore } from '@/stores/company'
|
||||
|
||||
const { t } = useI18n()
|
||||
const queryClient = useQueryClient()
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const createdRange = ref<[Dayjs, Dayjs] | null>(null)
|
||||
const draftFilters = reactive({
|
||||
domain: '',
|
||||
title: '',
|
||||
})
|
||||
const appliedFilters = reactive({
|
||||
domain: '',
|
||||
title: '',
|
||||
created_from: '',
|
||||
created_to: '',
|
||||
})
|
||||
const modalOpen = ref(false)
|
||||
const editingArticle = ref<MonitoringMarkedArticle | null>(null)
|
||||
const formState = reactive({
|
||||
article_title: '',
|
||||
original_url: '',
|
||||
})
|
||||
|
||||
const listParams = computed(() => ({
|
||||
page: page.value,
|
||||
page_size: pageSize.value,
|
||||
domain: appliedFilters.domain || undefined,
|
||||
title: appliedFilters.title || undefined,
|
||||
created_from: appliedFilters.created_from || undefined,
|
||||
created_to: appliedFilters.created_to || undefined,
|
||||
}))
|
||||
|
||||
const markedArticlesQuery = useQuery({
|
||||
queryKey: computed(() => ['tracking', 'marked-articles', listParams.value]),
|
||||
queryFn: () => monitoringApi.listMarkedArticles(listParams.value),
|
||||
})
|
||||
|
||||
const markedArticles = computed(() => markedArticlesQuery.data.value?.items ?? [])
|
||||
const pageInfo = computed(() => markedArticlesQuery.data.value?.page)
|
||||
const total = computed(() => pageInfo.value?.total ?? 0)
|
||||
const modalTitle = computed(() =>
|
||||
editingArticle.value
|
||||
? t('trackingMarkedArticles.editTitle')
|
||||
: t('trackingMarkedArticles.createTitle'),
|
||||
)
|
||||
|
||||
const columns = computed<TableColumnsType<MonitoringMarkedArticle>>(() => [
|
||||
{
|
||||
title: t('trackingMarkedArticles.columns.articleTitle'),
|
||||
dataIndex: 'article_title',
|
||||
key: 'article_title',
|
||||
width: 320,
|
||||
},
|
||||
{
|
||||
title: t('trackingMarkedArticles.columns.domain'),
|
||||
dataIndex: 'registrable_domain',
|
||||
key: 'registrable_domain',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: t('trackingMarkedArticles.columns.url'),
|
||||
dataIndex: 'original_url',
|
||||
key: 'original_url',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: t('trackingMarkedArticles.columns.markedAt'),
|
||||
dataIndex: 'marked_at',
|
||||
key: 'marked_at',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: t('trackingMarkedArticles.columns.expiresAt'),
|
||||
dataIndex: 'expires_at',
|
||||
key: 'expires_at',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: t('common.actions'),
|
||||
key: 'actions',
|
||||
width: 140,
|
||||
fixed: 'right',
|
||||
align: 'right' as const,
|
||||
},
|
||||
])
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (payload: MonitoringMarkedArticleRequest) =>
|
||||
monitoringApi.createMarkedArticle(payload),
|
||||
onSuccess: async () => {
|
||||
message.success(t('trackingMarkedArticles.messages.saved'))
|
||||
closeModal()
|
||||
await invalidateMarkedArticles()
|
||||
},
|
||||
onError: (error) => message.error(formatError(error)),
|
||||
})
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: (payload: { id: number; body: MonitoringMarkedArticleRequest }) =>
|
||||
monitoringApi.updateMarkedArticle(payload.id, payload.body),
|
||||
onSuccess: async () => {
|
||||
message.success(t('trackingMarkedArticles.messages.saved'))
|
||||
closeModal()
|
||||
await invalidateMarkedArticles()
|
||||
},
|
||||
onError: (error) => message.error(formatError(error)),
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => monitoringApi.deleteMarkedArticle(id),
|
||||
onSuccess: async () => {
|
||||
message.success(t('trackingMarkedArticles.messages.deleted'))
|
||||
await invalidateMarkedArticles()
|
||||
},
|
||||
onError: (error) => message.error(formatError(error)),
|
||||
})
|
||||
|
||||
const saving = computed(() => createMutation.isPending.value || updateMutation.isPending.value)
|
||||
|
||||
function applyFilters(): void {
|
||||
page.value = 1
|
||||
appliedFilters.domain = draftFilters.domain.trim()
|
||||
appliedFilters.title = draftFilters.title.trim()
|
||||
appliedFilters.created_from = createdRange.value?.[0]?.toISOString() ?? ''
|
||||
appliedFilters.created_to = createdRange.value?.[1]?.toISOString() ?? ''
|
||||
}
|
||||
|
||||
function resetFilters(): void {
|
||||
draftFilters.domain = ''
|
||||
draftFilters.title = ''
|
||||
createdRange.value = null
|
||||
applyFilters()
|
||||
}
|
||||
|
||||
function openCreateModal(): void {
|
||||
editingArticle.value = null
|
||||
formState.article_title = ''
|
||||
formState.original_url = ''
|
||||
modalOpen.value = true
|
||||
}
|
||||
|
||||
function openEditModal(record: MonitoringMarkedArticle): void {
|
||||
editingArticle.value = record
|
||||
formState.article_title = record.article_title
|
||||
formState.original_url = record.original_url
|
||||
modalOpen.value = true
|
||||
}
|
||||
|
||||
function closeModal(): void {
|
||||
modalOpen.value = false
|
||||
editingArticle.value = null
|
||||
formState.article_title = ''
|
||||
formState.original_url = ''
|
||||
}
|
||||
|
||||
async function submitForm(): Promise<void> {
|
||||
const payload: MonitoringMarkedArticleRequest = {
|
||||
article_title: formState.article_title.trim(),
|
||||
original_url: formState.original_url.trim(),
|
||||
brand_id: companyStore.currentBrandId ?? null,
|
||||
}
|
||||
if (!payload.article_title || !payload.original_url) {
|
||||
message.warning(t('trackingMarkedArticles.messages.required'))
|
||||
return
|
||||
}
|
||||
if (editingArticle.value) {
|
||||
await updateMutation.mutateAsync({ id: editingArticle.value.id, body: payload })
|
||||
return
|
||||
}
|
||||
await createMutation.mutateAsync(payload)
|
||||
}
|
||||
|
||||
function confirmDelete(record: MonitoringMarkedArticle): void {
|
||||
Modal.confirm({
|
||||
title: t('trackingMarkedArticles.deleteConfirmTitle'),
|
||||
content: record.article_title,
|
||||
okText: t('common.delete'),
|
||||
okType: 'danger',
|
||||
cancelText: t('common.cancel'),
|
||||
onOk: () => deleteMutation.mutateAsync(record.id),
|
||||
})
|
||||
}
|
||||
|
||||
function openURL(url: string): void {
|
||||
const normalized = url.trim()
|
||||
if (!normalized) {
|
||||
return
|
||||
}
|
||||
window.open(normalized, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
function handleTableChange(pagination: TablePaginationConfig): void {
|
||||
page.value = pagination.current ?? 1
|
||||
pageSize.value = pagination.pageSize ?? pageSize.value
|
||||
}
|
||||
|
||||
function renderTotal(count: number): string {
|
||||
return t('trackingMarkedArticles.total', { count })
|
||||
}
|
||||
|
||||
async function invalidateMarkedArticles(): Promise<void> {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['tracking', 'marked-articles'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['tracking'] }),
|
||||
])
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="marked-articles-page">
|
||||
<PageHero
|
||||
:title="t('trackingMarkedArticles.title')"
|
||||
:description="t('trackingMarkedArticles.description')"
|
||||
>
|
||||
<template #actions>
|
||||
<a-button @click="markedArticlesQuery.refetch()">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
{{ t('common.refresh') }}
|
||||
</a-button>
|
||||
<a-button type="primary" @click="openCreateModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
{{ t('trackingMarkedArticles.createAction') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</PageHero>
|
||||
|
||||
<a-card class="marked-articles-card">
|
||||
<div class="marked-articles-filters">
|
||||
<a-input
|
||||
v-model:value="draftFilters.domain"
|
||||
allow-clear
|
||||
class="marked-articles-filters__domain"
|
||||
:placeholder="t('trackingMarkedArticles.filters.domain')"
|
||||
@pressEnter="applyFilters"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<a-input
|
||||
v-model:value="draftFilters.title"
|
||||
allow-clear
|
||||
class="marked-articles-filters__title"
|
||||
:placeholder="t('trackingMarkedArticles.filters.title')"
|
||||
@pressEnter="applyFilters"
|
||||
/>
|
||||
<a-range-picker
|
||||
v-model:value="createdRange"
|
||||
allow-clear
|
||||
show-time
|
||||
class="marked-articles-filters__range"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
:placeholder="[
|
||||
t('trackingMarkedArticles.filters.createdFrom'),
|
||||
t('trackingMarkedArticles.filters.createdTo'),
|
||||
]"
|
||||
/>
|
||||
<div class="marked-articles-filters__actions">
|
||||
<a-button type="primary" @click="applyFilters">{{ t('common.search') }}</a-button>
|
||||
<a-button @click="resetFilters">{{ t('common.reset') }}</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
row-key="id"
|
||||
class="modern-table marked-articles-table"
|
||||
:columns="columns"
|
||||
:data-source="markedArticles"
|
||||
:loading="markedArticlesQuery.isLoading.value"
|
||||
:pagination="{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: renderTotal,
|
||||
}"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'article_title'">
|
||||
<div class="marked-article-title">
|
||||
<strong :title="record.article_title">{{ record.article_title }}</strong>
|
||||
<span>{{ record.publish_platform }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'registrable_domain'">
|
||||
<a-tag color="blue">{{ record.registrable_domain }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'original_url'">
|
||||
<a
|
||||
:href="record.original_url"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="marked-article-url"
|
||||
:title="record.original_url"
|
||||
>
|
||||
{{ record.original_url }}
|
||||
</a>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'marked_at'">
|
||||
{{ formatDateTime(record.marked_at) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'expires_at'">
|
||||
{{ formatDateTime(record.expires_at) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<div class="marked-article-actions">
|
||||
<a-tooltip :title="t('trackingMarkedArticles.openOriginal')">
|
||||
<a-button type="text" shape="circle" @click="openURL(record.original_url)">
|
||||
<template #icon><EyeOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="t('common.edit')">
|
||||
<a-button type="text" shape="circle" @click="openEditModal(record)">
|
||||
<template #icon><EditOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="t('common.delete')">
|
||||
<a-button
|
||||
type="text"
|
||||
danger
|
||||
shape="circle"
|
||||
:loading="deleteMutation.isPending.value"
|
||||
@click="confirmDelete(record)"
|
||||
>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalOpen"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="saving"
|
||||
width="560px"
|
||||
@ok="submitForm"
|
||||
@cancel="closeModal"
|
||||
>
|
||||
<a-form layout="vertical" class="marked-article-form">
|
||||
<a-form-item :label="t('trackingMarkedArticles.form.articleTitle')" required>
|
||||
<a-input
|
||||
v-model:value="formState.article_title"
|
||||
:maxlength="160"
|
||||
:placeholder="t('trackingMarkedArticles.form.articleTitlePlaceholder')"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('trackingMarkedArticles.form.originalUrl')" required>
|
||||
<a-input
|
||||
v-model:value="formState.original_url"
|
||||
:placeholder="t('trackingMarkedArticles.form.originalUrlPlaceholder')"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.marked-articles-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.marked-articles-card {
|
||||
border: 1px solid #edf1f7;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.marked-articles-filters {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 240px) minmax(220px, 1fr) minmax(280px, 360px) auto;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.marked-articles-filters__actions {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.marked-articles-table :deep(.ant-table-cell) {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.marked-article-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.marked-article-title strong {
|
||||
overflow: hidden;
|
||||
color: #111827;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.marked-article-title span {
|
||||
color: #8a96aa;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.marked-article-url {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
color: #2563eb;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.marked-article-actions {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.marked-article-form {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.marked-articles-filters {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.marked-articles-filters__range,
|
||||
.marked-articles-filters__actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.marked-articles-filters__actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.marked-articles-filters {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowLeftOutlined, CopyOutlined } from '@ant-design/icons-vue'
|
||||
import { ArrowLeftOutlined, BookOutlined, CopyOutlined } from '@ant-design/icons-vue'
|
||||
import type {
|
||||
MonitoringQuestionCitationAnalysisItem,
|
||||
MonitoringQuestionDetailCitation,
|
||||
MonitoringQuestionDetailPlatform,
|
||||
MonitoringSourceItem,
|
||||
} from '@geo/shared-types'
|
||||
import { useQuery } from '@tanstack/vue-query'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { message } from 'ant-design-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -22,6 +23,7 @@ import { useCompanyStore } from '@/stores/company'
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
const companyStore = useCompanyStore()
|
||||
|
||||
const trackingMaxHistoryDays = 30
|
||||
@@ -179,6 +181,7 @@ type TrackingAnalyzedContentCitation = {
|
||||
citationCount: number
|
||||
citationRate: number | null
|
||||
citedURL: string | null
|
||||
sourceType: string | null
|
||||
}
|
||||
|
||||
type TrackingCitationSourceLookupItem = {
|
||||
@@ -202,6 +205,20 @@ const shouldShowInlineContentCitations = computed(() => {
|
||||
return activeInlineContentCitations.value.length > 0
|
||||
})
|
||||
|
||||
const markCitationMutation = useMutation({
|
||||
mutationFn: (citation: MonitoringQuestionDetailCitation) =>
|
||||
monitoringApi.createMarkedArticle({
|
||||
article_title: resolveExternalArticleTitle(citation),
|
||||
original_url: citation.cited_url,
|
||||
brand_id: brandId.value ?? null,
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
message.success(t('tracking.markedArticleSaved'))
|
||||
await queryClient.invalidateQueries({ queryKey: ['tracking'] })
|
||||
},
|
||||
onError: (error) => message.error(formatError(error)),
|
||||
})
|
||||
|
||||
watch(activePlatformId, () => {
|
||||
clearCitationHighlight()
|
||||
})
|
||||
@@ -248,6 +265,13 @@ function openImitationCreate(
|
||||
})
|
||||
}
|
||||
|
||||
function markCitationAsExternal(citation: MonitoringQuestionDetailCitation): void {
|
||||
if (!canMarkCitationAsExternal(citation, activePlatform.value?.ai_platform_id)) {
|
||||
return
|
||||
}
|
||||
markCitationMutation.mutate(citation)
|
||||
}
|
||||
|
||||
function formatPercent(value: number | null | undefined): string {
|
||||
if (value === null || value === undefined) {
|
||||
return '--'
|
||||
@@ -311,6 +335,37 @@ function resolveCitationTitle(citation: MonitoringQuestionDetailCitation): strin
|
||||
return citation.cited_title || citation.article_title || citation.cited_url
|
||||
}
|
||||
|
||||
function resolveExternalArticleTitle(citation: MonitoringQuestionDetailCitation): string {
|
||||
const title = String(citation.cited_title ?? citation.article_title ?? '').trim()
|
||||
if (title) {
|
||||
return title
|
||||
}
|
||||
return String(citation.site_name ?? citation.cited_url).trim()
|
||||
}
|
||||
|
||||
function isManualMarkedCitation(
|
||||
citation: MonitoringQuestionDetailCitation | null | undefined,
|
||||
): boolean {
|
||||
return citation?.source_type === 'manual_mark'
|
||||
}
|
||||
|
||||
function canMarkCitationAsExternal(
|
||||
citation: MonitoringQuestionDetailCitation | null | undefined,
|
||||
platformId: string | null | undefined,
|
||||
): boolean {
|
||||
if (!citation || !supportsContentCitationDisplay(platformId)) {
|
||||
return false
|
||||
}
|
||||
if (!String(citation.cited_url ?? '').trim()) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
!citation.article_id &&
|
||||
citation.source_type !== 'published_article' &&
|
||||
!isManualMarkedCitation(citation)
|
||||
)
|
||||
}
|
||||
|
||||
function isLinkedInlineCitationPlatform(platformId: string | null | undefined): boolean {
|
||||
return platformId === 'yuanbao'
|
||||
}
|
||||
@@ -1118,6 +1173,7 @@ function analyzeInlineContentCitations(
|
||||
citationCount: item.citationCount,
|
||||
citationRate: item.citationCount / totalCitationCount,
|
||||
citedURL: item.url,
|
||||
sourceType: null,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1151,6 +1207,7 @@ function analyzeInlineContentCitations(
|
||||
citationCount: item.citationCount,
|
||||
citationRate: item.citationCount / totalCitationCount,
|
||||
citedURL: item.url,
|
||||
sourceType: null,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1199,6 +1256,7 @@ function analyzeInlineContentCitations(
|
||||
citationCount,
|
||||
citationRate: citationCount / totalCitationCount,
|
||||
citedURL,
|
||||
sourceType: citation?.source_type ?? null,
|
||||
},
|
||||
]
|
||||
})
|
||||
@@ -1503,17 +1561,36 @@ const contentCitationColumns = computed(() => [
|
||||
>
|
||||
{{ resolveCitationTitle(citation) }}
|
||||
</span>
|
||||
<a-tooltip :title="t('tracking.imitationAction')">
|
||||
<button
|
||||
type="button"
|
||||
class="tracking-question-imitation-trigger tracking-question-imitation-trigger--source"
|
||||
@click.stop="
|
||||
openImitationCreate(citation.cited_url, resolveCitationTitle(citation))
|
||||
"
|
||||
<div class="tracking-question-source-card__actions">
|
||||
<a-tooltip
|
||||
v-if="canMarkCitationAsExternal(citation, activePlatform?.ai_platform_id)"
|
||||
:title="t('tracking.markAsExternalArticle')"
|
||||
>
|
||||
<CopyOutlined />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
<button
|
||||
type="button"
|
||||
class="tracking-question-imitation-trigger"
|
||||
:disabled="markCitationMutation.isPending.value"
|
||||
@click.stop="markCitationAsExternal(citation)"
|
||||
@keydown.enter.stop.prevent="markCitationAsExternal(citation)"
|
||||
>
|
||||
<BookOutlined />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="t('tracking.imitationAction')">
|
||||
<button
|
||||
type="button"
|
||||
class="tracking-question-imitation-trigger"
|
||||
@click.stop="
|
||||
openImitationCreate(citation.cited_url, resolveCitationTitle(citation))
|
||||
"
|
||||
@keydown.enter.stop.prevent="
|
||||
openImitationCreate(citation.cited_url, resolveCitationTitle(citation))
|
||||
"
|
||||
>
|
||||
<CopyOutlined />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<a-tag
|
||||
v-if="
|
||||
citation.article_id &&
|
||||
@@ -1524,6 +1601,16 @@ const contentCitationColumns = computed(() => [
|
||||
>
|
||||
{{ t('tracking.contentCitationBadge') }}
|
||||
</a-tag>
|
||||
<a-tag
|
||||
v-else-if="
|
||||
isManualMarkedCitation(citation) &&
|
||||
supportsContentCitationDisplay(activePlatform?.ai_platform_id)
|
||||
"
|
||||
color="green"
|
||||
class="tracking-question-source-card__badge"
|
||||
>
|
||||
{{ t('tracking.manualMarkedArticle') }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="tracking-question-source-card__link">
|
||||
{{ citation.cited_url }}
|
||||
@@ -1627,16 +1714,20 @@ const contentCitationColumns = computed(() => [
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'contentName'">
|
||||
<div class="cell-primary-stack">
|
||||
<a
|
||||
v-if="record.citedURL"
|
||||
:href="record.citedURL"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
:title="record.contentName"
|
||||
class="table-link"
|
||||
>
|
||||
{{ record.contentName }}
|
||||
</a>
|
||||
<template v-if="record.citedURL">
|
||||
<a
|
||||
:href="record.citedURL"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
:title="record.contentName"
|
||||
class="table-link"
|
||||
>
|
||||
{{ record.contentName }}
|
||||
</a>
|
||||
<small v-if="record.sourceType === 'manual_mark'" class="meta-subtext">
|
||||
{{ t('tracking.manualMarkedArticle') }}
|
||||
</small>
|
||||
</template>
|
||||
<strong v-else class="table-text" :title="record.contentName">
|
||||
{{ record.contentName }}
|
||||
</strong>
|
||||
@@ -2139,10 +2230,17 @@ const contentCitationColumns = computed(() => [
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.tracking-question-imitation-trigger--source {
|
||||
.tracking-question-imitation-trigger:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.tracking-question-source-card__actions {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 16px;
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
transform: translateY(calc(-50% + 1px));
|
||||
}
|
||||
|
||||
@@ -2152,9 +2250,8 @@ const contentCitationColumns = computed(() => [
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.tracking-question-source-card:hover .tracking-question-imitation-trigger--source,
|
||||
.tracking-question-source-card:focus-within .tracking-question-imitation-trigger--source {
|
||||
opacity: 1;
|
||||
.tracking-question-source-card:hover .tracking-question-source-card__actions,
|
||||
.tracking-question-source-card:focus-within .tracking-question-source-card__actions {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
EyeOutlined,
|
||||
ReloadOutlined,
|
||||
QuestionCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
EyeOutlined,
|
||||
FileTextOutlined,
|
||||
QuestionCircleOutlined,
|
||||
ReloadOutlined,
|
||||
RightOutlined,
|
||||
SearchOutlined,
|
||||
FileTextOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import type {
|
||||
DesktopAccountInfo,
|
||||
@@ -189,7 +189,10 @@ watch(
|
||||
() => route.query.question_id,
|
||||
(value) => {
|
||||
const requestedQuestionId = parseNumericQuery(route.query.question_id)
|
||||
if (missingRequestedQuestionId.value && missingRequestedQuestionId.value !== requestedQuestionId) {
|
||||
if (
|
||||
missingRequestedQuestionId.value &&
|
||||
missingRequestedQuestionId.value !== requestedQuestionId
|
||||
) {
|
||||
missingRequestedQuestionId.value = null
|
||||
}
|
||||
if (value || selectedBrandId.value) {
|
||||
@@ -691,9 +694,18 @@ watch([selectedBrandId, selectedQuestionId, selectedPlatformId, selectedBusiness
|
||||
hotQuestionPage.value = 1
|
||||
})
|
||||
|
||||
watch([selectedBrandId, selectedQuestionId, selectedPlatformId, selectedBusinessDate, selectedCitationWindowDays], () => {
|
||||
citedArticlePage.value = 1
|
||||
})
|
||||
watch(
|
||||
[
|
||||
selectedBrandId,
|
||||
selectedQuestionId,
|
||||
selectedPlatformId,
|
||||
selectedBusinessDate,
|
||||
selectedCitationWindowDays,
|
||||
],
|
||||
() => {
|
||||
citedArticlePage.value = 1
|
||||
},
|
||||
)
|
||||
|
||||
function openQuestion(question: MonitoringHotQuestion): void {
|
||||
if (!selectedBrandId.value) {
|
||||
@@ -718,10 +730,30 @@ function openQuestion(question: MonitoringHotQuestion): void {
|
||||
}
|
||||
|
||||
function openCitedArticle(item: MonitoringCitedArticle): void {
|
||||
if (item.source_type === 'manual_mark') {
|
||||
openExternalURL(item.source_url)
|
||||
return
|
||||
}
|
||||
if (!item.article_id) {
|
||||
openExternalURL(item.source_url)
|
||||
return
|
||||
}
|
||||
selectedArticleId.value = item.article_id
|
||||
articleDrawerOpen.value = true
|
||||
}
|
||||
|
||||
function citedArticleKey(item: MonitoringCitedArticle): string {
|
||||
return `${item.source_type}:${item.source_id || item.source_url || item.article_id || item.article_title}`
|
||||
}
|
||||
|
||||
function openExternalURL(url: string | null | undefined): void {
|
||||
const normalized = String(url ?? '').trim()
|
||||
if (!normalized) {
|
||||
return
|
||||
}
|
||||
window.open(normalized, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
function formatPercent(value: number | null | undefined): string {
|
||||
if (value === null || value === undefined) {
|
||||
return '--'
|
||||
@@ -1308,7 +1340,10 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
||||
<strong>{{ question.question_text }}</strong>
|
||||
<div class="mention-rate-container">
|
||||
<span class="mention-rate-label">{{ t('tracking.metrics.mentionRate') }}</span>
|
||||
<span class="mention-rate-badge" :class="getMentionRateClass(question.mention_rate)">
|
||||
<span
|
||||
class="mention-rate-badge"
|
||||
:class="getMentionRateClass(question.mention_rate)"
|
||||
>
|
||||
{{ formatPercent(question.mention_rate) }}
|
||||
</span>
|
||||
<div class="mention-rate-track">
|
||||
@@ -1368,7 +1403,7 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
||||
<div v-if="citedArticles.length" class="article-list">
|
||||
<div
|
||||
v-for="item in citedArticles"
|
||||
:key="item.article_id"
|
||||
:key="citedArticleKey(item)"
|
||||
class="article-list__item"
|
||||
>
|
||||
<div class="article-list__item-left">
|
||||
@@ -1377,7 +1412,13 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
||||
</div>
|
||||
<div class="article-list__item-content">
|
||||
<strong>{{ item.article_title }}</strong>
|
||||
<span class="article-platform-tag">{{ item.publish_platform }}</span>
|
||||
<span class="article-platform-tag">
|
||||
{{
|
||||
item.source_type === 'manual_mark'
|
||||
? t('tracking.manualMarkedArticle')
|
||||
: item.publish_platform
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="article-list__side">
|
||||
@@ -1693,7 +1734,9 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
||||
.tracking-list-search.ant-input-affix-wrapper:focus {
|
||||
background-color: #ffffff;
|
||||
border-color: #1f5cff;
|
||||
box-shadow: 0 0 0 3px rgba(31, 92, 255, 0.1), 0 4px 12px rgba(31, 92, 255, 0.05);
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(31, 92, 255, 0.1),
|
||||
0 4px 12px rgba(31, 92, 255, 0.05);
|
||||
}
|
||||
|
||||
.tracking-list-search :deep(.ant-input) {
|
||||
@@ -2035,7 +2078,9 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
||||
.detail-citation-list__item:hover {
|
||||
background: #ffffff;
|
||||
border-color: rgba(31, 92, 255, 0.25);
|
||||
box-shadow: 0 10px 25px -5px rgba(31, 92, 255, 0.08), 0 8px 10px -6px rgba(31, 92, 255, 0.04);
|
||||
box-shadow:
|
||||
0 10px 25px -5px rgba(31, 92, 255, 0.08),
|
||||
0 8px 10px -6px rgba(31, 92, 255, 0.04);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user