feat(admin-web): extract AI account management into its own page
Move the AI-platform account status cards out of TrackingView into a dedicated AIAccountsView, registered at /tracking/ai-accounts with a nav entry and route/nav i18n strings. Keeps the tracking dashboard focused while giving accounts, authorization state and desktop runtime nodes a first-class page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,7 @@ const enUS = {
|
||||
tracking: 'Tracking',
|
||||
trackingDetail: 'Data Details',
|
||||
trackingMarkedArticles: 'External Article Marks',
|
||||
aiAccounts: 'AI Account Management',
|
||||
contentManagement: 'Content Management',
|
||||
knowledge: 'Knowledge Base',
|
||||
images: 'Image Management',
|
||||
@@ -270,6 +271,10 @@ const enUS = {
|
||||
description:
|
||||
'Manage manually marked external article URLs so they count in cited articles and citation rankings.',
|
||||
},
|
||||
aiAccounts: {
|
||||
title: 'AI Account Management',
|
||||
description: 'Review AI model accounts, authorization state, and desktop runtime nodes.',
|
||||
},
|
||||
trackingQuestion: {
|
||||
title: 'Question Detail',
|
||||
description:
|
||||
|
||||
@@ -92,6 +92,7 @@ const zhCN = {
|
||||
tracking: '数据追踪',
|
||||
trackingDetail: '数据详情',
|
||||
trackingMarkedArticles: '外部文章标记',
|
||||
aiAccounts: 'AI 账号管理',
|
||||
contentManagement: '内容管理',
|
||||
knowledge: '知识库',
|
||||
images: '图片管理',
|
||||
@@ -269,6 +270,10 @@ const zhCN = {
|
||||
title: '外部文章标记',
|
||||
description: '维护用户手动标记的外部文章 URL,让它们参与引用文章和引用排行统计。',
|
||||
},
|
||||
aiAccounts: {
|
||||
title: 'AI 账号管理',
|
||||
description: '查看 AI 模型账号、授权状态和桌面运行节点。',
|
||||
},
|
||||
trackingQuestion: {
|
||||
title: '问题详情',
|
||||
description: '按问题查看大模型回答、引用来源、引用分析和回答引用。',
|
||||
@@ -564,7 +569,8 @@ const zhCN = {
|
||||
backToQuestions: '返回问题列表',
|
||||
citationRanking: 'Citation Ranking',
|
||||
citationRankingTitle: '引用排行',
|
||||
citationRankingHint: '按发布外链、投稿外链和手动标记 URL 归因外部文章在模型引用来源中的出现次数与占比。',
|
||||
citationRankingHint:
|
||||
'按发布外链、投稿外链和手动标记 URL 归因外部文章在模型引用来源中的出现次数与占比。',
|
||||
citationWindow7: '近 7 天',
|
||||
citationWindow30: '近 30 天',
|
||||
citationWindowLabel: '近 {days} 天',
|
||||
@@ -697,7 +703,8 @@ const zhCN = {
|
||||
},
|
||||
trackingMarkedArticles: {
|
||||
title: '外部文章标记',
|
||||
description: '把无法自动对接的官网、媒体或第三方页面标记为外部文章,系统会在 6 个月内用于引用归因。',
|
||||
description:
|
||||
'把无法自动对接的官网、媒体或第三方页面标记为外部文章,系统会在 6 个月内用于引用归因。',
|
||||
createAction: '标记文章',
|
||||
createTitle: '标记外部文章',
|
||||
editTitle: '编辑外部文章',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ApiOutlined,
|
||||
AppstoreOutlined,
|
||||
BankOutlined,
|
||||
BarChartOutlined,
|
||||
@@ -384,6 +385,11 @@ const navSections = computed<NavSection[]>(() => {
|
||||
label: t('nav.trackingMarkedArticles'),
|
||||
icon: FileTextOutlined,
|
||||
},
|
||||
{
|
||||
key: '/tracking/ai-accounts',
|
||||
label: t('nav.aiAccounts'),
|
||||
icon: ApiOutlined,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -249,6 +249,16 @@ const router = createRouter({
|
||||
navKey: '/tracking/marked-articles',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'tracking/ai-accounts',
|
||||
name: 'tracking-ai-accounts',
|
||||
component: () => import('@/views/AIAccountsView.vue'),
|
||||
meta: {
|
||||
titleKey: 'route.aiAccounts.title',
|
||||
descriptionKey: 'route.aiAccounts.description',
|
||||
navKey: '/tracking/ai-accounts',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'tracking/questions/:brandId/:questionId',
|
||||
name: 'tracking-question-detail',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,6 @@ import {
|
||||
SearchOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import type {
|
||||
DesktopAccountInfo,
|
||||
MonitoringCitedArticle,
|
||||
MonitoringHotQuestion,
|
||||
MonitoringPlatformAuthorizationStatus,
|
||||
@@ -25,12 +24,10 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import ArticleDetailDrawer from '@/components/ArticleDetailDrawer.vue'
|
||||
import PageHero from '@/components/PageHero.vue'
|
||||
import { monitoringApi, tenantAccountsApi } from '@/lib/api'
|
||||
import { resolveAccountCheckedAt, resolveAccountHealth } from '@/lib/desktop-account-runtime'
|
||||
import { monitoringApi } from '@/lib/api'
|
||||
import { formatDateTime } from '@/lib/display'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import {
|
||||
isMonitoringPlatformId,
|
||||
normalizeMonitoringPlatformFilter,
|
||||
normalizeMonitoringPlatformId,
|
||||
} from '@/lib/monitoring-platforms'
|
||||
@@ -62,19 +59,6 @@ type CitationRankingRow = {
|
||||
glyph: string
|
||||
}
|
||||
|
||||
type AIPlatformStatusCard = {
|
||||
id: string
|
||||
label: string
|
||||
shortName: string
|
||||
accent: string
|
||||
description: string
|
||||
account: DesktopAccountInfo | null
|
||||
isClientOnline: boolean
|
||||
sampleStatus: string
|
||||
lastSampledAt: string | null
|
||||
actualSampleCount: number
|
||||
}
|
||||
|
||||
function formatTrackingBusinessDate(value: Date): string {
|
||||
const parts = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone: trackingBusinessTimeZone,
|
||||
@@ -366,11 +350,6 @@ const citationSummaryQuery = useQuery({
|
||||
}),
|
||||
})
|
||||
|
||||
const aiAccountsQuery = useQuery({
|
||||
queryKey: ['tracking', 'ai-platform-accounts'],
|
||||
queryFn: () => tenantAccountsApi.list(),
|
||||
})
|
||||
|
||||
const collectNowMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!selectedBrandId.value) {
|
||||
@@ -475,61 +454,6 @@ const hotQuestionsPageInfo = computed(() => {
|
||||
|
||||
const hotQuestionsTotal = computed(() => hotQuestionsPageInfo.value?.total ?? 0)
|
||||
|
||||
const aiPlatformStatusCards = computed<AIPlatformStatusCard[]>(() => {
|
||||
const accountGroups = new Map<string, DesktopAccountInfo[]>()
|
||||
for (const account of aiAccountsQuery.data.value ?? []) {
|
||||
const platformId = normalizeMonitoringPlatformId(account.platform)
|
||||
if (!isMonitoringPlatformId(platformId) || account.deleted_at) {
|
||||
continue
|
||||
}
|
||||
const list = accountGroups.get(platformId) ?? []
|
||||
list.push(account)
|
||||
accountGroups.set(platformId, list)
|
||||
}
|
||||
|
||||
const breakdownMap = new Map(
|
||||
(dashboardQuery.data.value?.platform_breakdown ?? []).map((item) => [
|
||||
normalizeMonitoringPlatformId(item.ai_platform_id),
|
||||
item,
|
||||
]),
|
||||
)
|
||||
const onlineAuthorizedPlatformIDs = new Set(
|
||||
(dashboardQuery.data.value?.runtime.online_authorized_monitoring_platform_ids ?? [])
|
||||
.map((item) => normalizeMonitoringPlatformId(item))
|
||||
.filter((item): item is string => Boolean(item)),
|
||||
)
|
||||
|
||||
return aiPlatformCatalog.map((platform) => {
|
||||
const matchedAccounts = [...(accountGroups.get(platform.id) ?? [])].sort((left, right) => {
|
||||
const leftOnline = left.client_online === true ? 1 : 0
|
||||
const rightOnline = right.client_online === true ? 1 : 0
|
||||
if (leftOnline !== rightOnline) {
|
||||
return rightOnline - leftOnline
|
||||
}
|
||||
const leftAt = Date.parse(resolveAccountCheckedAt(left) ?? '') || 0
|
||||
const rightAt = Date.parse(resolveAccountCheckedAt(right) ?? '') || 0
|
||||
return rightAt - leftAt
|
||||
})
|
||||
const breakdown = breakdownMap.get(platform.id)
|
||||
|
||||
return {
|
||||
id: platform.id,
|
||||
label: platform.label,
|
||||
shortName: platform.shortName,
|
||||
accent: platform.accent,
|
||||
description: platform.description,
|
||||
account: matchedAccounts[0] ?? null,
|
||||
isClientOnline:
|
||||
onlineAuthorizedPlatformIDs.has(platform.id) ||
|
||||
(!dashboardQuery.data.value &&
|
||||
matchedAccounts.some((account) => account.client_online === true)),
|
||||
sampleStatus: breakdown?.platform_sample_status ?? 'pending',
|
||||
lastSampledAt: breakdown?.last_sampled_at ?? null,
|
||||
actualSampleCount: breakdown?.actual_sample_count ?? 0,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const selectedMetricCards = computed(() => {
|
||||
const overview = dashboardQuery.data.value?.overview
|
||||
return [
|
||||
@@ -820,48 +744,6 @@ function normalizeCitationWindowDays(value: unknown): number {
|
||||
return numeric === 30 ? 30 : 7
|
||||
}
|
||||
|
||||
function accountAuthLabel(account: DesktopAccountInfo | null): string {
|
||||
if (!account) {
|
||||
return '未绑定'
|
||||
}
|
||||
|
||||
switch (resolveAccountHealth(account)) {
|
||||
case 'live':
|
||||
return '授权正常'
|
||||
case 'captcha':
|
||||
return '待处理'
|
||||
case 'expired':
|
||||
return '授权过期'
|
||||
default:
|
||||
return '风险观察'
|
||||
}
|
||||
}
|
||||
|
||||
function accountAuthColor(account: DesktopAccountInfo | null): string {
|
||||
if (!account) {
|
||||
return 'default'
|
||||
}
|
||||
|
||||
switch (resolveAccountHealth(account)) {
|
||||
case 'live':
|
||||
return 'green'
|
||||
case 'captcha':
|
||||
return 'gold'
|
||||
case 'expired':
|
||||
return 'red'
|
||||
default:
|
||||
return 'orange'
|
||||
}
|
||||
}
|
||||
|
||||
function desktopStatusLabel(account: DesktopAccountInfo | null, isClientOnline: boolean): string {
|
||||
if (!account?.client_id) {
|
||||
return '未绑定桌面端'
|
||||
}
|
||||
|
||||
return isClientOnline ? '客户端在线' : '客户端离线'
|
||||
}
|
||||
|
||||
function formatCount(value: number | null | undefined): string {
|
||||
if (value === null || value === undefined) {
|
||||
return '--'
|
||||
@@ -1075,7 +957,9 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
||||
]"
|
||||
>
|
||||
<span v-if="(card.value ?? 0) > (card.previous ?? 0)" class="trend-arrow">↑</span>
|
||||
<span v-else-if="(card.value ?? 0) < (card.previous ?? 0)" class="trend-arrow">↓</span>
|
||||
<span v-else-if="(card.value ?? 0) < (card.previous ?? 0)" class="trend-arrow">
|
||||
↓
|
||||
</span>
|
||||
{{ (formatDelta(card.value, card.previous) ?? '').replace(/[+-]/, '') }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -1130,80 +1014,6 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<a-card
|
||||
class="tracking-panel tracking-panel--ai-platforms"
|
||||
:loading="dashboardQuery.isLoading.value || aiAccountsQuery.isLoading.value"
|
||||
>
|
||||
<div class="tracking-panel__header">
|
||||
<div>
|
||||
<h3 class="panel-title">AI 平台状态</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ai-platform-status-grid">
|
||||
<article
|
||||
v-for="platform in aiPlatformStatusCards"
|
||||
:key="platform.id"
|
||||
class="ai-platform-status-card"
|
||||
:style="{ '--platform-accent': platform.accent }"
|
||||
>
|
||||
<div class="ai-platform-status-card__head">
|
||||
<div class="ai-platform-status-card__identity">
|
||||
<span
|
||||
class="ai-platform-status-card__badge"
|
||||
:style="{
|
||||
backgroundColor: `${platform.accent}14`,
|
||||
color: platform.accent,
|
||||
borderColor: `${platform.accent}30`,
|
||||
}"
|
||||
>
|
||||
{{ platform.shortName }}
|
||||
</span>
|
||||
<div>
|
||||
<strong>{{ platform.label }}</strong>
|
||||
<p>{{ platform.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-tag :color="accountAuthColor(platform.account)">
|
||||
{{ accountAuthLabel(platform.account) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
|
||||
<div class="ai-platform-status-card__meta">
|
||||
<div class="ai-platform-status-card__row">
|
||||
<span>绑定状态</span>
|
||||
<strong>{{ platform.account ? '已绑定' : '未绑定' }}</strong>
|
||||
</div>
|
||||
<div class="ai-platform-status-card__row">
|
||||
<span>客户端</span>
|
||||
<strong>{{ desktopStatusLabel(platform.account, platform.isClientOnline) }}</strong>
|
||||
</div>
|
||||
<div class="ai-platform-status-card__row">
|
||||
<span>采样状态</span>
|
||||
<a-tag :color="statusTagColor(platform.sampleStatus)">
|
||||
{{ statusLabel(platform.sampleStatus) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="ai-platform-status-card__row">
|
||||
<span>最近采样</span>
|
||||
<strong>
|
||||
{{ platform.lastSampledAt ? formatDateTime(platform.lastSampledAt) : '--' }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="ai-platform-status-card__row">
|
||||
<span>样本数</span>
|
||||
<strong>{{ formatCount(platform.actualSampleCount) }}</strong>
|
||||
</div>
|
||||
<div v-if="platform.account" class="ai-platform-status-card__row">
|
||||
<span>账号</span>
|
||||
<strong>{{ platform.account.display_name }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<div class="tracking-grid tracking-grid--middle">
|
||||
<a-card
|
||||
class="tracking-panel tracking-panel--citation"
|
||||
@@ -1462,7 +1272,7 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
||||
? t('tracking.manualMarkedArticle')
|
||||
: item.source_type === 'media_supply_order_item'
|
||||
? t('tracking.mediaSupplySubmissionCitation')
|
||||
: item.publish_platform
|
||||
: item.publish_platform
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
@@ -1579,105 +1389,6 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.tracking-panel--ai-platforms {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ai-platform-status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.ai-platform-status-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #ffffff;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.ai-platform-status-card:hover {
|
||||
border-color: var(--platform-accent) !important;
|
||||
box-shadow: 0 10px 20px -8px var(--platform-accent);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.ai-platform-status-card__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.ai-platform-status-card__identity {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.ai-platform-status-card__identity strong {
|
||||
display: block;
|
||||
color: #0f172a;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.ai-platform-status-card__identity p {
|
||||
margin: 4px 0 0;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ai-platform-status-card__badge {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10px;
|
||||
border: 1px solid transparent;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ai-platform-status-card__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ai-platform-status-card__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.ai-platform-status-card__row span {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ai-platform-status-card__row strong {
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.tracking-citation-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1715,7 +1426,10 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
||||
border-radius: 16px;
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(226, 232, 240, 0.8) !important;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.02), 0 10px 15px -3px rgba(0, 0, 0, 0.03), 0 0 0 1px rgba(0, 0, 0, 0.01);
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.02),
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.03),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.01);
|
||||
}
|
||||
|
||||
.tracking-panel--citation {
|
||||
@@ -1845,7 +1559,9 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
|
||||
.metric-card:hover {
|
||||
border-color: #3b82f6;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 15px -3px rgba(59, 130, 246, 0.05), 0 4px 6px -2px rgba(59, 130, 246, 0.02);
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(59, 130, 246, 0.05),
|
||||
0 4px 6px -2px rgba(59, 130, 246, 0.02);
|
||||
}
|
||||
|
||||
.metric-card__label {
|
||||
|
||||
Reference in New Issue
Block a user