feat(workspace): replace supported platform count with bound media account count
The "platforms" stat tile was a constant (5) that never reflected what the tenant actually had set up. Drop SupportedPlatformCount from the workspace overview API/domain/shared-types and have admin-web compute a bound media account count from the desktop accounts list, filtered through a new isMediaPublishAccount helper that excludes deleted rows and AI-only platforms (yuanbao/kimi/wenxin/deepseek/doubao/qwen). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -379,7 +379,7 @@ const enUS = {
|
|||||||
articles: "Generated",
|
articles: "Generated",
|
||||||
published: "Published",
|
published: "Published",
|
||||||
brands: "Brands",
|
brands: "Brands",
|
||||||
platforms: "Platforms",
|
mediaAccounts: "Media accounts",
|
||||||
},
|
},
|
||||||
sections: {
|
sections: {
|
||||||
generalTemplates: "General-Purpose Templates",
|
generalTemplates: "General-Purpose Templates",
|
||||||
|
|||||||
@@ -380,7 +380,7 @@ const zhCN = {
|
|||||||
articles: "生成数",
|
articles: "生成数",
|
||||||
published: "发布数",
|
published: "发布数",
|
||||||
brands: "品牌数",
|
brands: "品牌数",
|
||||||
platforms: "媒体平台数",
|
mediaAccounts: "媒体账号数",
|
||||||
},
|
},
|
||||||
sections: {
|
sections: {
|
||||||
generalTemplates: "普通通用模版",
|
generalTemplates: "普通通用模版",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { resolveApiURL } from "@/lib/api";
|
|||||||
import { resolveAccountCheckedAt, resolveAccountHealth } from "@/lib/desktop-account-runtime";
|
import { resolveAccountCheckedAt, resolveAccountHealth } from "@/lib/desktop-account-runtime";
|
||||||
import {
|
import {
|
||||||
getPublishPlatformMeta,
|
getPublishPlatformMeta,
|
||||||
|
isMediaPublishAccount,
|
||||||
isPublishPlatformId,
|
isPublishPlatformId,
|
||||||
normalizePublishPlatformId,
|
normalizePublishPlatformId,
|
||||||
} from "@/lib/publish-platforms";
|
} from "@/lib/publish-platforms";
|
||||||
@@ -65,7 +66,7 @@ export function buildPublishAccountCards(
|
|||||||
|
|
||||||
return accounts
|
return accounts
|
||||||
.filter((account) => {
|
.filter((account) => {
|
||||||
if (account.deleted_at) {
|
if (!isMediaPublishAccount(account)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const platformId = normalizePublishPlatformId(account.platform);
|
const platformId = normalizePublishPlatformId(account.platform);
|
||||||
|
|||||||
@@ -25,9 +25,19 @@ const platformCatalog: Record<string, Omit<PublishPlatformOption, "accountLabel"
|
|||||||
dongchedi: { id: "dongchedi", name: "懂车帝", shortName: "懂", accent: "#fadb14" },
|
dongchedi: { id: "dongchedi", name: "懂车帝", shortName: "懂", accent: "#fadb14" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const aiPlatformIds = new Set(["yuanbao", "kimi", "wenxin", "deepseek", "doubao", "qwen"]);
|
||||||
|
|
||||||
|
export function isAIPlatformId(platformId?: string | null): boolean {
|
||||||
|
return aiPlatformIds.has(normalizePublishPlatformId(platformId));
|
||||||
|
}
|
||||||
|
|
||||||
export function isPublishPlatformId(platformId?: string | null): boolean {
|
export function isPublishPlatformId(platformId?: string | null): boolean {
|
||||||
const normalized = normalizePublishPlatformId(platformId);
|
const normalized = normalizePublishPlatformId(platformId);
|
||||||
return Boolean(normalized) && normalized in platformCatalog;
|
return Boolean(normalized) && !isAIPlatformId(normalized) && normalized in platformCatalog;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMediaPublishAccount(account: DesktopAccountInfo): boolean {
|
||||||
|
return !account.deleted_at && isPublishPlatformId(account.platform);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPublishPlatformMeta(platformId: string): Omit<PublishPlatformOption, "accountLabel" | "bound"> {
|
export function getPublishPlatformMeta(platformId: string): Omit<PublishPlatformOption, "accountLabel" | "bound"> {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useI18n } from "vue-i18n";
|
|||||||
import { mediaApi, resolveApiURL, tenantAccountsApi } from "@/lib/api";
|
import { mediaApi, resolveApiURL, tenantAccountsApi } from "@/lib/api";
|
||||||
import { resolveAccountCheckedAt, resolveAccountHealth } from "@/lib/desktop-account-runtime";
|
import { resolveAccountCheckedAt, resolveAccountHealth } from "@/lib/desktop-account-runtime";
|
||||||
import { formatDateTime } from "@/lib/display";
|
import { formatDateTime } from "@/lib/display";
|
||||||
import { getPublishPlatformMeta, isPublishPlatformId, normalizePublishPlatformId } from "@/lib/publish-platforms";
|
import { getPublishPlatformMeta, isMediaPublishAccount, normalizePublishPlatformId } from "@/lib/publish-platforms";
|
||||||
|
|
||||||
type AccountHealthFilter = "all" | "live" | "captcha" | "risk" | "expired";
|
type AccountHealthFilter = "all" | "live" | "captcha" | "risk" | "expired";
|
||||||
type PublishState = "immediate" | "queued" | "unavailable";
|
type PublishState = "immediate" | "queued" | "unavailable";
|
||||||
@@ -79,8 +79,7 @@ const platformMap = computed(() => {
|
|||||||
|
|
||||||
const mediaAccounts = computed<MediaAccountCard[]>(() => {
|
const mediaAccounts = computed<MediaAccountCard[]>(() => {
|
||||||
return [...(accountsQuery.data.value ?? [])]
|
return [...(accountsQuery.data.value ?? [])]
|
||||||
.filter((account) => !account.deleted_at)
|
.filter(isMediaPublishAccount)
|
||||||
.filter((account) => isPublishPlatformId(account.platform))
|
|
||||||
.map((account) => {
|
.map((account) => {
|
||||||
const platformId = normalizePublishPlatformId(account.platform);
|
const platformId = normalizePublishPlatformId(account.platform);
|
||||||
const platform = platformMap.value.get(platformId);
|
const platform = platformMap.value.get(platformId);
|
||||||
|
|||||||
@@ -26,10 +26,11 @@ import ArticlePublishStatus from "@/components/ArticlePublishStatus.vue";
|
|||||||
import ArticleSourceMeta from "@/components/ArticleSourceMeta.vue";
|
import ArticleSourceMeta from "@/components/ArticleSourceMeta.vue";
|
||||||
import ArticleDetailDrawer from "@/components/ArticleDetailDrawer.vue";
|
import ArticleDetailDrawer from "@/components/ArticleDetailDrawer.vue";
|
||||||
import PublishArticleModal from "@/components/PublishArticleModal.vue";
|
import PublishArticleModal from "@/components/PublishArticleModal.vue";
|
||||||
import { articlesApi, workspaceApi } from "@/lib/api";
|
import { articlesApi, tenantAccountsApi, workspaceApi } from "@/lib/api";
|
||||||
import { buildArticleClipboardContent, resolveArticleActionState } from "@/lib/article-list-actions";
|
import { buildArticleClipboardContent, resolveArticleActionState } from "@/lib/article-list-actions";
|
||||||
import { formatDateTime, getTemplateMeta } from "@/lib/display";
|
import { formatDateTime, getTemplateMeta } from "@/lib/display";
|
||||||
import { formatError } from "@/lib/errors";
|
import { formatError } from "@/lib/errors";
|
||||||
|
import { isMediaPublishAccount } from "@/lib/publish-platforms";
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -44,6 +45,10 @@ const overviewQuery = useQuery({
|
|||||||
queryKey: ["workspace", "overview"],
|
queryKey: ["workspace", "overview"],
|
||||||
queryFn: () => workspaceApi.overview(),
|
queryFn: () => workspaceApi.overview(),
|
||||||
});
|
});
|
||||||
|
const accountsQuery = useQuery({
|
||||||
|
queryKey: ["tenant", "desktop-accounts", "workspace-overview"],
|
||||||
|
queryFn: () => tenantAccountsApi.list(),
|
||||||
|
});
|
||||||
const recentArticlesQuery = useQuery({
|
const recentArticlesQuery = useQuery({
|
||||||
queryKey: ["workspace", "recent-articles"],
|
queryKey: ["workspace", "recent-articles"],
|
||||||
queryFn: () => workspaceApi.recentArticles(),
|
queryFn: () => workspaceApi.recentArticles(),
|
||||||
@@ -83,13 +88,17 @@ const statIcons = [
|
|||||||
PlaySquareOutlined,
|
PlaySquareOutlined,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const boundMediaAccountCount = computed(() =>
|
||||||
|
(accountsQuery.data.value ?? []).filter(isMediaPublishAccount).length,
|
||||||
|
);
|
||||||
|
|
||||||
const statItems = computed(() => {
|
const statItems = computed(() => {
|
||||||
const overview = overviewQuery.data.value;
|
const overview = overviewQuery.data.value;
|
||||||
return [
|
return [
|
||||||
{ label: t("workspace.metrics.articles"), value: overview?.article_count ?? 0, icon: statIcons[0] },
|
{ label: t("workspace.metrics.articles"), value: overview?.article_count ?? 0, icon: statIcons[0] },
|
||||||
{ label: t("workspace.metrics.published"), value: overview?.published_count ?? 0, icon: statIcons[1] },
|
{ label: t("workspace.metrics.published"), value: overview?.published_count ?? 0, icon: statIcons[1] },
|
||||||
{ label: t("workspace.metrics.brands"), value: overview?.brand_count ?? 0, icon: statIcons[2] },
|
{ label: t("workspace.metrics.brands"), value: overview?.brand_count ?? 0, icon: statIcons[2] },
|
||||||
{ label: t("workspace.metrics.platforms"), value: overview?.supported_platform_count ?? 0, icon: statIcons[3] },
|
{ label: t("workspace.metrics.mediaAccounts"), value: boundMediaAccountCount.value, icon: statIcons[3] },
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -231,6 +240,7 @@ async function handleDelete(articleId: number): Promise<void> {
|
|||||||
function refreshDashboard(): void {
|
function refreshDashboard(): void {
|
||||||
void Promise.all([
|
void Promise.all([
|
||||||
overviewQuery.refetch(),
|
overviewQuery.refetch(),
|
||||||
|
accountsQuery.refetch(),
|
||||||
recentArticlesQuery.refetch(),
|
recentArticlesQuery.refetch(),
|
||||||
templateCardsQuery.refetch(),
|
templateCardsQuery.refetch(),
|
||||||
kolCardsQuery.refetch(),
|
kolCardsQuery.refetch(),
|
||||||
|
|||||||
@@ -334,7 +334,6 @@ export interface WorkspaceOverview {
|
|||||||
article_count: number;
|
article_count: number;
|
||||||
published_count: number;
|
published_count: number;
|
||||||
brand_count: number;
|
brand_count: number;
|
||||||
supported_platform_count: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RecentArticle {
|
export interface RecentArticle {
|
||||||
|
|||||||
@@ -17,13 +17,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type WorkspaceService struct {
|
type WorkspaceService struct {
|
||||||
repo repository.WorkspaceRepository
|
repo repository.WorkspaceRepository
|
||||||
templates repository.TemplateRepository
|
templates repository.TemplateRepository
|
||||||
quota repository.QuotaRepository
|
quota repository.QuotaRepository
|
||||||
aiPointUsage repository.AIPointUsageRepository
|
aiPointUsage repository.AIPointUsageRepository
|
||||||
supportedPlatformCount int
|
cache sharedcache.Cache
|
||||||
cache sharedcache.Cache
|
cacheGroup singleflight.Group
|
||||||
cacheGroup singleflight.Group
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWorkspaceService(
|
func NewWorkspaceService(
|
||||||
@@ -33,11 +32,10 @@ func NewWorkspaceService(
|
|||||||
aiPointUsage repository.AIPointUsageRepository,
|
aiPointUsage repository.AIPointUsageRepository,
|
||||||
) *WorkspaceService {
|
) *WorkspaceService {
|
||||||
return &WorkspaceService{
|
return &WorkspaceService{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
templates: templates,
|
templates: templates,
|
||||||
quota: quota,
|
quota: quota,
|
||||||
aiPointUsage: aiPointUsage,
|
aiPointUsage: aiPointUsage,
|
||||||
supportedPlatformCount: 5,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,12 +59,10 @@ func (s *WorkspaceService) Overview(ctx context.Context) (*domain.WorkspaceOverv
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, response.ErrInternal(50010, "query_failed", "failed to count brands")
|
return nil, response.ErrInternal(50010, "query_failed", "failed to count brands")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &domain.WorkspaceOverview{
|
return &domain.WorkspaceOverview{
|
||||||
ArticleCount: articleCount,
|
ArticleCount: articleCount,
|
||||||
PublishedCount: publishedCount,
|
PublishedCount: publishedCount,
|
||||||
BrandCount: brandCount,
|
BrandCount: brandCount,
|
||||||
SupportedPlatformCount: s.supportedPlatformCount,
|
|
||||||
}, nil
|
}, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ type WorkspaceOverview struct {
|
|||||||
ArticleCount int64 `json:"article_count"`
|
ArticleCount int64 `json:"article_count"`
|
||||||
PublishedCount int64 `json:"published_count"`
|
PublishedCount int64 `json:"published_count"`
|
||||||
BrandCount int64 `json:"brand_count"`
|
BrandCount int64 `json:"brand_count"`
|
||||||
SupportedPlatformCount int `json:"supported_platform_count"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecentArticle struct {
|
type RecentArticle struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user