diff --git a/apps/admin-web/src/i18n/messages/en-US.ts b/apps/admin-web/src/i18n/messages/en-US.ts index 0e92fe6..5c9223f 100644 --- a/apps/admin-web/src/i18n/messages/en-US.ts +++ b/apps/admin-web/src/i18n/messages/en-US.ts @@ -81,6 +81,7 @@ const enUS = { brandManagement: 'Brand Management', tracking: 'Tracking', trackingDetail: 'Data Details', + trackingMarkedArticles: 'External Article Marks', contentManagement: 'Content Management', knowledge: 'Knowledge Base', images: 'Image Management', @@ -264,6 +265,11 @@ const enUS = { description: 'Inspect plugin-sampled brand performance, platform coverage, hot questions, and citation attribution.', }, + trackingMarkedArticles: { + title: 'External Article Marks', + description: + 'Manage manually marked external article URLs so they count in cited articles and citation rankings.', + }, trackingQuestion: { title: 'Question Detail', description: @@ -579,7 +585,7 @@ const enUS = { citationRanking: 'Citation Ranking', citationRankingTitle: 'Citation Ranking', citationRankingHint: - 'Attribute SaaS-published content by published-link identity across model citation sources.', + 'Attribute external articles by published links and manually marked URLs across model citation sources.', citationWindow7: 'Last 7 days', citationWindow30: 'Last 30 days', citationWindowLabel: 'Last {days} days', @@ -597,6 +603,9 @@ const enUS = { taskDebugHint: 'For troubleshooting the callback -> queue -> worker flow only. The four main content sections above stay unchanged.', contentCitationBadge: 'Content Citation', + manualMarkedArticle: 'External Article', + markAsExternalArticle: 'Mark as external article', + markedArticleSaved: 'Marked as external article.', noQuestions: 'No hot questions for the current filters', noCitationRanking: 'No attributable citations in the current window', noCitedArticles: 'No content citations in the current window', @@ -708,6 +717,41 @@ const enUS = { }, }, }, + trackingMarkedArticles: { + title: 'External Article Marks', + description: + 'Mark official-site, media, or third-party pages as external articles when they cannot be connected automatically. Marks are retained for 6 months.', + createAction: 'Mark Article', + createTitle: 'Mark External Article', + editTitle: 'Edit External Article', + openOriginal: 'Open original page', + deleteConfirmTitle: 'Delete this external article mark?', + total: '{count} total', + filters: { + domain: 'Search domain, e.g. example.com', + title: 'Search article title', + createdFrom: 'Start time', + createdTo: 'End time', + }, + form: { + articleTitle: 'Article title', + articleTitlePlaceholder: 'Enter article title', + originalUrl: 'Original URL', + originalUrlPlaceholder: 'https://example.com/article', + }, + columns: { + articleTitle: 'Article Title', + domain: 'Domain', + url: 'Original URL', + markedAt: 'Marked At', + expiresAt: 'Retained Until', + }, + messages: { + required: 'Enter article title and original URL.', + saved: 'External article mark saved.', + deleted: 'External article mark deleted.', + }, + }, templateMeta: { top_x_article: { eyebrow: 'Ranked Content', diff --git a/apps/admin-web/src/i18n/messages/zh-CN.ts b/apps/admin-web/src/i18n/messages/zh-CN.ts index a7032af..ce7602f 100644 --- a/apps/admin-web/src/i18n/messages/zh-CN.ts +++ b/apps/admin-web/src/i18n/messages/zh-CN.ts @@ -91,6 +91,7 @@ const zhCN = { brands: '品牌和词库', tracking: '数据追踪', trackingDetail: '数据详情', + trackingMarkedArticles: '外部文章标记', contentManagement: '内容管理', knowledge: '知识库', images: '图片管理', @@ -264,6 +265,10 @@ const zhCN = { title: '数据详情', description: '围绕插件采样快照查看品牌表现、平台矩阵、高频问题和引用归因。', }, + trackingMarkedArticles: { + title: '外部文章标记', + description: '维护用户手动标记的外部文章 URL,让它们参与引用文章和引用排行统计。', + }, trackingQuestion: { title: '问题详情', description: '按问题查看大模型回答、引用来源、引用分析和内容引用。', @@ -561,7 +566,7 @@ const zhCN = { backToQuestions: '返回问题列表', citationRanking: 'Citation Ranking', citationRankingTitle: '引用排行', - citationRankingHint: '按发布外链主体归因 SaaS 发文在模型引用来源中的出现次数与占比。', + citationRankingHint: '按发布外链和手动标记 URL 归因外部文章在模型引用来源中的出现次数与占比。', citationWindow7: '近 7 天', citationWindow30: '近 30 天', citationWindowLabel: '近 {days} 天', @@ -578,6 +583,9 @@ const zhCN = { taskDebugTitle: '异步任务调试', taskDebugHint: '仅用于排查 callback -> queue -> worker 的异步流转,不影响上方四块主内容。', contentCitationBadge: '内容引用', + manualMarkedArticle: '外部文章', + markAsExternalArticle: '标记为外部文章', + markedArticleSaved: '已标记为外部文章', noQuestions: '当前筛选条件下暂无高频问题', noCitationRanking: '当前时间窗口内暂无可归因引用', noCitedArticles: '当前时间窗口内暂无内容引用', @@ -688,6 +696,40 @@ const zhCN = { }, }, }, + trackingMarkedArticles: { + title: '外部文章标记', + description: '把无法自动对接的官网、媒体或第三方页面标记为外部文章,系统会在 6 个月内用于引用归因。', + createAction: '标记文章', + createTitle: '标记外部文章', + editTitle: '编辑外部文章', + openOriginal: '打开原始页面', + deleteConfirmTitle: '删除这条外部文章标记?', + total: '共 {count} 条', + filters: { + domain: '按域名搜索,如 example.com', + title: '按文章名称搜索', + createdFrom: '开始时间', + createdTo: '结束时间', + }, + form: { + articleTitle: '文章名称', + articleTitlePlaceholder: '输入文章标题', + originalUrl: '原始 URL', + originalUrlPlaceholder: 'https://example.com/article', + }, + columns: { + articleTitle: '文章名称', + domain: '域名', + url: '原始 URL', + markedAt: '标记时间', + expiresAt: '保留至', + }, + messages: { + required: '请填写文章名称和原始 URL', + saved: '外部文章标记已保存', + deleted: '外部文章标记已删除', + }, + }, templateMeta: { top_x_article: { eyebrow: '榜单内容', diff --git a/apps/admin-web/src/layouts/AppShell.vue b/apps/admin-web/src/layouts/AppShell.vue index d4cb5ef..3609c28 100644 --- a/apps/admin-web/src/layouts/AppShell.vue +++ b/apps/admin-web/src/layouts/AppShell.vue @@ -377,7 +377,14 @@ const navSections = computed(() => { { key: 'tracking', title: t('nav.tracking'), - items: [{ key: '/tracking', label: t('nav.trackingDetail'), icon: LineChartOutlined }], + items: [ + { key: '/tracking', label: t('nav.trackingDetail'), icon: LineChartOutlined }, + { + key: '/tracking/marked-articles', + label: t('nav.trackingMarkedArticles'), + icon: FileTextOutlined, + }, + ], }, { key: 'contentManagement', diff --git a/apps/admin-web/src/lib/api.ts b/apps/admin-web/src/lib/api.ts index 9861990..8a1bd54 100644 --- a/apps/admin-web/src/lib/api.ts +++ b/apps/admin-web/src/lib/api.ts @@ -103,6 +103,9 @@ import type { MonitoringCitationSummaryResponse, MonitoringCollectNowResponse, MonitoringDashboardCompositeResponse, + MonitoringMarkedArticle, + MonitoringMarkedArticleListResponse, + MonitoringMarkedArticleRequest, MonitoringQuestionDetailResponse, PasswordPublicKeyResponse, PlatformAccount, @@ -1533,6 +1536,34 @@ export const monitoringApi = { }, ) }, + listMarkedArticles(params?: { + page?: number + page_size?: number + domain?: string + title?: string + created_from?: string + created_to?: string + }) { + return apiClient.get( + '/api/tenant/monitoring/marked-articles', + { params }, + ) + }, + createMarkedArticle(payload: MonitoringMarkedArticleRequest) { + return apiClient.post( + '/api/tenant/monitoring/marked-articles', + payload, + ) + }, + updateMarkedArticle(id: number, payload: MonitoringMarkedArticleRequest) { + return apiClient.put( + `/api/tenant/monitoring/marked-articles/${id}`, + payload, + ) + }, + deleteMarkedArticle(id: number) { + return apiClient.remove<{ ok: boolean }>(`/api/tenant/monitoring/marked-articles/${id}`) + }, questionDetail( brandId: number, questionId: number, diff --git a/apps/admin-web/src/lib/display.ts b/apps/admin-web/src/lib/display.ts index c76385c..a684c82 100644 --- a/apps/admin-web/src/lib/display.ts +++ b/apps/admin-web/src/lib/display.ts @@ -26,11 +26,24 @@ const publishStatusMap: Record = { blocked_by_compliance: { label: 'status.publish.blocked_by_compliance', color: 'error' }, } +const goTimestampWithZonePattern = + /^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})(\.\d+)?\s+([+-]\d{2})(\d{2})(?:\s+\S+)?$/ + +function normalizeDateTimeValue(value: string): string { + const trimmed = value.trim() + const match = trimmed.match(goTimestampWithZonePattern) + if (!match) { + return trimmed + } + const [, date, time, fraction = '', hourOffset, minuteOffset] = match + return `${date}T${time}${fraction}${hourOffset}:${minuteOffset}` +} + export function formatDateTime(value?: string | null, pattern = 'YYYY-MM-DD HH:mm'): string { if (!value) { return '--' } - const parsed = dayjs(value) + const parsed = dayjs(normalizeDateTimeValue(value)) return parsed.isValid() ? parsed.format(pattern) : value } diff --git a/apps/admin-web/src/router/index.ts b/apps/admin-web/src/router/index.ts index d67d3a2..c84f0a3 100644 --- a/apps/admin-web/src/router/index.ts +++ b/apps/admin-web/src/router/index.ts @@ -239,6 +239,16 @@ const router = createRouter({ navKey: '/tracking', }, }, + { + path: 'tracking/marked-articles', + name: 'tracking-marked-articles', + component: () => import('@/views/TrackingMarkedArticlesView.vue'), + meta: { + titleKey: 'route.trackingMarkedArticles.title', + descriptionKey: 'route.trackingMarkedArticles.description', + navKey: '/tracking/marked-articles', + }, + }, { path: 'tracking/questions/:brandId/:questionId', name: 'tracking-question-detail', diff --git a/apps/admin-web/src/views/TrackingMarkedArticlesView.vue b/apps/admin-web/src/views/TrackingMarkedArticlesView.vue new file mode 100644 index 0000000..f8636ca --- /dev/null +++ b/apps/admin-web/src/views/TrackingMarkedArticlesView.vue @@ -0,0 +1,472 @@ + + + + + diff --git a/apps/admin-web/src/views/TrackingQuestionDetailView.vue b/apps/admin-web/src/views/TrackingQuestionDetailView.vue index f78b4a4..9ad14aa 100644 --- a/apps/admin-web/src/views/TrackingQuestionDetailView.vue +++ b/apps/admin-web/src/views/TrackingQuestionDetailView.vue @@ -1,12 +1,13 @@