feat(admin-web): surface monitoring platform authorization status

Dashboard and question-detail views now read platform_authorization_status
from the API. Tracking disables collect-now when the account has no
desktop client, no authorized platforms, or the selected platform is
unauthorized, and keeps the six-platform matrix stable; question-detail
replaces tabs and cards with an explicit unavailable notice when the
account is not authorized for monitoring. Adds the shared type and
en/zh strings covering each status.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 22:21:01 +08:00
parent c0253c98f9
commit 1b1ab7ee0c
5 changed files with 137 additions and 10 deletions
@@ -65,6 +65,21 @@ const detailQuery = useQuery({
const activePlatformId = ref("");
const detailPlatforms = computed<MonitoringQuestionDetailPlatform[]>(() => detailQuery.data.value?.platforms ?? []);
const hasNoDetailPlatforms = computed(() => {
return Boolean(detailQuery.data.value) && detailPlatforms.value.length === 0;
});
const shouldShowDetailUnavailableNotice = computed(() => {
return hasNoDetailPlatforms.value;
});
const detailUnavailableNoticeType = computed<"warning" | "info">(() => {
return "info";
});
const detailUnavailableNoticeTitle = computed(() => {
return requestedPlatformId.value ? t("tracking.selectedPlatformUnauthorizedTitle") : t("tracking.noDetailPlatformsTitle");
});
const detailUnavailableNoticeDescription = computed(() => {
return requestedPlatformId.value ? t("tracking.selectedPlatformUnauthorizedDescription") : t("tracking.noDetailPlatformsDescription");
});
const detailCitationAnalysis = computed<MonitoringQuestionCitationAnalysisItem[]>(() => detailQuery.data.value?.citation_analysis ?? []);
const citationSourcesScrollRef = ref<HTMLElement | null>(null);
const highlightedCitationIndex = ref<number | null>(null);
@@ -919,7 +934,19 @@ const contentCitationColumns = computed(() => [
<a-spin :spinning="detailQuery.isLoading.value">
<div class="tracking-question-page__content">
<div class="tracking-question-page__tabs-wrapper">
<a-alert
v-if="shouldShowDetailUnavailableNotice"
:type="detailUnavailableNoticeType"
show-icon
:message="detailUnavailableNoticeTitle"
:description="detailUnavailableNoticeDescription"
class="tracking-question-page__alert"
/>
<div
v-if="!shouldShowDetailUnavailableNotice"
class="tracking-question-page__tabs-wrapper"
>
<a-tabs
v-model:activeKey="activePlatformId"
size="large"
@@ -938,7 +965,10 @@ const contentCitationColumns = computed(() => [
</a-tabs>
</div>
<div class="tracking-question-grid">
<div
v-if="!shouldShowDetailUnavailableNotice"
class="tracking-question-grid"
>
<section class="tracking-question-card tracking-question-card--answer">
<div class="tracking-question-card__header">
<div class="tracking-question-card__section-title">
@@ -1035,7 +1065,7 @@ const contentCitationColumns = computed(() => [
</div>
</div>
<div class="tracking-question-card__scroll-area custom-scrollbar" style="margin: 0; padding: 0;">
<div class="tracking-question-card__scroll-area custom-scrollbar tracking-question-table-wrap">
<a-table
v-if="activeCitationAnalysis.length"
row-key="site_key"
@@ -1065,7 +1095,7 @@ const contentCitationColumns = computed(() => [
</template>
</template>
</a-table>
<a-empty v-else :description="t('tracking.noCitationAnalysis')" style="margin-top: 40px;" />
<a-empty v-else :description="t('tracking.noCitationAnalysis')" class="tracking-question-empty" />
</div>
</section>
@@ -1078,7 +1108,7 @@ const contentCitationColumns = computed(() => [
</div>
</div>
<div class="tracking-question-card__scroll-area custom-scrollbar" style="margin: 0; padding: 0;">
<div class="tracking-question-card__scroll-area custom-scrollbar tracking-question-table-wrap">
<a-table
row-key="key"
class="modern-table"
@@ -1311,6 +1341,15 @@ const contentCitationColumns = computed(() => [
padding-right: 12px;
}
.tracking-question-table-wrap {
margin: 0;
padding: 0;
}
.tracking-question-empty {
margin-top: 40px;
}
.tracking-question-answer__headline {
display: flex;
align-items: center;
+40 -5
View File
@@ -10,6 +10,7 @@ import type {
Brand,
DesktopAccountInfo,
MonitoringHotQuestion,
MonitoringPlatformAuthorizationStatus,
} from "@geo/shared-types";
import { aiPlatformCatalog } from "@geo/shared-types";
import { computed, watch } from "vue";
@@ -283,6 +284,22 @@ const currentUserClientOnline = computed(() => {
return dashboardQuery.data.value?.runtime.current_user_client_online === true;
});
const platformBreakdownRows = computed(() => {
return dashboardQuery.data.value?.platform_breakdown ?? [];
});
const platformAuthorizationStatus = computed<MonitoringPlatformAuthorizationStatus>(() => {
return dashboardQuery.data.value?.runtime.platform_authorization_status ?? "authorized";
});
const selectedPlatformAuthorized = computed(() => {
if (selectedPlatformId.value === "all") {
return true;
}
const authorizedPlatformIDs = dashboardQuery.data.value?.runtime.authorized_monitoring_platform_ids ?? [];
return authorizedPlatformIDs.some((item) => normalizeMonitoringPlatformId(item) === selectedPlatformId.value);
});
const collectNowDisabledReason = computed(() => {
if (!selectedBrandId.value) {
return t("tracking.collectBrandRequired");
@@ -296,6 +313,15 @@ const collectNowDisabledReason = computed(() => {
if (dashboardQuery.isLoading.value && !dashboardQuery.data.value) {
return t("tracking.collectClientChecking");
}
if (platformAuthorizationStatus.value === "no_desktop_client") {
return t("tracking.noDesktopClientCollectReason");
}
if (platformAuthorizationStatus.value === "no_authorized_platforms") {
return t("tracking.noAuthorizedPlatformsCollectReason");
}
if (selectedPlatformId.value !== "all" && dashboardQuery.data.value && !selectedPlatformAuthorized.value) {
return t("tracking.collectPlatformUnauthorized");
}
if (!currentUserClientOnline.value) {
return t("tracking.collectClientOnlineRequired");
}
@@ -625,7 +651,7 @@ function normalizeTrackingBusinessDate(value: unknown): string | null {
return null;
}
const today = dayjs().startOf("day");
const today = dayjs(trackingToday).startOf("day");
const earliestAllowed = today.subtract(trackingMaxHistoryDays - 1, "day");
if (parsed.isAfter(today, "day") || parsed.isBefore(earliestAllowed, "day")) {
return null;
@@ -634,8 +660,8 @@ function normalizeTrackingBusinessDate(value: unknown): string | null {
}
function disableTrackingDate(current: dayjs.Dayjs): boolean {
const today = dayjs().endOf("day");
const earliestAllowed = dayjs().subtract(trackingMaxHistoryDays - 1, "day").startOf("day");
const today = dayjs(trackingToday).endOf("day");
const earliestAllowed = dayjs(trackingToday).subtract(trackingMaxHistoryDays - 1, "day").startOf("day");
return current.isAfter(today) || current.isBefore(earliestAllowed);
}
</script>
@@ -739,7 +765,7 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
<h3 class="panel-title">{{ t("tracking.platformMatrixTitle") }}</h3>
</div>
<div class="platform-table">
<div v-if="platformBreakdownRows.length" class="platform-table">
<div class="platform-table__head">
<span>{{ t("tracking.columns.platform") }}</span>
<span>{{ t("tracking.metrics.mentionRate") }}</span>
@@ -748,7 +774,7 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
</div>
<div
v-for="item in dashboardQuery.data.value?.platform_breakdown ?? []"
v-for="item in platformBreakdownRows"
:key="item.ai_platform_id"
class="platform-table__row"
>
@@ -763,6 +789,11 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
</a-tag>
</div>
</div>
<a-empty
v-else
:description="t('tracking.noPlatformBreakdown')"
class="tracking-panel__empty"
/>
</a-card>
</div>
@@ -1188,6 +1219,10 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
font-size: 13px;
}
.tracking-panel__empty {
padding: 28px 0 16px;
}
.tracking-metric-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));