feat(desktop): drop parked review flow, add publish management
SaaS 侧人工审核后才创建 publish job,desktop 不再做二次审核:移除 manual/waiting_user/parked/from_parked 状态机与 LeaseFromParked 查询, desktop client 只执行发布并新增"发布管理"页(待发布队列 / 历史 / 再次 发送)。同时抽离 @geo/publisher-platforms 共享适配器包、新增 Redis-based desktop_presence 与 publish_record_support,刷新 admin-web 发布弹窗与 媒体库;plan A / spec 文档同步口径。
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { QuestionCircleOutlined, UserOutlined } from "@ant-design/icons-vue";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import type { MediaPlatform, PlatformAccount } from "@geo/shared-types";
|
||||
import type { MediaPlatform } from "@geo/shared-types";
|
||||
import { computed, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
import { articlesApi, mediaApi, resolveApiURL } from "@/lib/api";
|
||||
import { articlesApi, mediaApi } from "@/lib/api";
|
||||
import { getPublishStatusMeta } from "@/lib/display";
|
||||
import { getPublishPlatformMeta, normalizePublishPlatformId } from "@/lib/publish-platforms";
|
||||
|
||||
@@ -49,13 +49,6 @@ const platformsQuery = useQuery({
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
const accountsQuery = useQuery({
|
||||
queryKey: ["media", "platform-accounts", "publish-status"],
|
||||
enabled: computed(() => popoverOpen.value && shouldLoadPlatforms.value),
|
||||
queryFn: () => mediaApi.accounts(),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
const publishRecordsQuery = useQuery({
|
||||
queryKey: computed(() => ["articles", "publish-records", props.articleId, "publish-status"]),
|
||||
enabled: computed(() => popoverOpen.value && shouldLoadPlatforms.value),
|
||||
@@ -73,12 +66,6 @@ const platformMap = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
const accountMap = computed(() => {
|
||||
return new Map(
|
||||
(accountsQuery.data.value ?? []).map((account: PlatformAccount) => [account.id, account]),
|
||||
);
|
||||
});
|
||||
|
||||
const platformEntries = computed<PublishPlatformEntry[]>(() => {
|
||||
const seen = new Set<string>();
|
||||
const entries: PublishPlatformEntry[] = [];
|
||||
@@ -92,7 +79,6 @@ const platformEntries = computed<PublishPlatformEntry[]>(() => {
|
||||
|
||||
const normalizedId = normalizePublishPlatformId(record.platform_id);
|
||||
const platform = platformMap.value.get(normalizedId);
|
||||
const account = accountMap.value.get(record.platform_account_id);
|
||||
const fallback = getPublishPlatformMeta(normalizedId);
|
||||
|
||||
entries.push({
|
||||
@@ -100,8 +86,8 @@ const platformEntries = computed<PublishPlatformEntry[]>(() => {
|
||||
name: record.platform_name || platform?.name || fallback.name,
|
||||
shortName: platform?.short_name || fallback.shortName,
|
||||
accent: platform?.accent_color || fallback.accent,
|
||||
accountName: account?.nickname || record.platform_nickname || "--",
|
||||
avatarUrl: resolveApiURL(account?.avatar_url),
|
||||
accountName: record.platform_nickname || "--",
|
||||
avatarUrl: "",
|
||||
status: normalizePublishRecordStatus(record.status),
|
||||
});
|
||||
}
|
||||
@@ -251,7 +237,7 @@ function getAccountInitial(accountName: string): string {
|
||||
<template #content>
|
||||
<div class="article-publish-status__popover">
|
||||
<div
|
||||
v-if="publishRecordsQuery.isPending.value || platformsQuery.isPending.value || accountsQuery.isPending.value"
|
||||
v-if="publishRecordsQuery.isPending.value || platformsQuery.isPending.value"
|
||||
class="article-publish-status__loading"
|
||||
>
|
||||
<a-spin size="small" />
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useI18n } from "vue-i18n";
|
||||
|
||||
import PromptRuleModal from "@/components/PromptRuleModal.vue";
|
||||
import PublishPlatformSelector from "@/components/PublishPlatformSelector.vue";
|
||||
import { generateApi, mediaApi, promptRulesApi, schedulesApi } from "@/lib/api";
|
||||
import { generateApi, mediaApi, promptRulesApi, schedulesApi, tenantAccountsApi } from "@/lib/api";
|
||||
import { formatError } from "@/lib/errors";
|
||||
import { IMAGE_UPLOAD_ACCEPT, useImageUploadProgress, validateImageUploadFile } from "@/lib/image-webp";
|
||||
import {
|
||||
@@ -55,9 +55,9 @@ const platformsQuery = useQuery({
|
||||
});
|
||||
|
||||
const accountsQuery = useQuery({
|
||||
queryKey: ["media", "platform-accounts", "task-drawer"],
|
||||
queryKey: ["tenant", "desktop-accounts", "task-drawer"],
|
||||
enabled: computed(() => props.open),
|
||||
queryFn: () => mediaApi.accounts(),
|
||||
queryFn: () => tenantAccountsApi.list(),
|
||||
});
|
||||
|
||||
const allPlatforms = computed(() =>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -152,7 +152,7 @@ const enUS = {
|
||||
},
|
||||
media: {
|
||||
title: "Media Management",
|
||||
description: "Manage media bindings, local extension state, and one-click publishing from a single control plane.",
|
||||
description: "Review desktop-managed media bindings, authorization state, and sync results in one place.",
|
||||
},
|
||||
brands: {
|
||||
title: "Brand Library",
|
||||
|
||||
@@ -152,7 +152,7 @@ const zhCN = {
|
||||
},
|
||||
media: {
|
||||
title: "媒体管理",
|
||||
description: "统一管理媒体账号绑定、本地插件状态和一键发布执行链路。",
|
||||
description: "统一查看桌面端媒体账号绑定、授权状态与同步结果。",
|
||||
},
|
||||
brands: {
|
||||
title: "品牌词库",
|
||||
|
||||
@@ -15,6 +15,8 @@ import type {
|
||||
CompetitorRequest,
|
||||
CreatePluginSessionRequest,
|
||||
CreatePluginSessionResponse,
|
||||
CreatePublishJobRequest,
|
||||
CreatePublishJobResponse,
|
||||
CreatePublishBatchRequest,
|
||||
CreatePublishBatchResponse,
|
||||
GenerateFromRuleRequest,
|
||||
@@ -104,6 +106,7 @@ import type {
|
||||
CreateKolPromptRequest,
|
||||
UpdateKolPackageRequest,
|
||||
UpdateKolPromptRequest,
|
||||
DesktopAccountInfo,
|
||||
UserInfo,
|
||||
WorkspaceOverview,
|
||||
} from "@geo/shared-types";
|
||||
@@ -869,7 +872,7 @@ export const brandsApi = {
|
||||
|
||||
export const mediaApi = {
|
||||
platforms() {
|
||||
return removedMediaApi<MediaPlatform[]>("platforms");
|
||||
return apiClient.get<MediaPlatform[]>("/api/tenant/media/platforms");
|
||||
},
|
||||
accounts() {
|
||||
return removedMediaApi<PlatformAccount[]>("accounts");
|
||||
@@ -888,6 +891,22 @@ export const mediaApi = {
|
||||
},
|
||||
};
|
||||
|
||||
export const tenantAccountsApi = {
|
||||
list() {
|
||||
return apiClient.get<DesktopAccountInfo[]>("/api/tenant/accounts");
|
||||
},
|
||||
requestDelete(id: string, options?: { undo?: boolean }) {
|
||||
const query = options?.undo ? "?undo=true" : "";
|
||||
return apiClient.post<DesktopAccountInfo>(`/api/tenant/accounts/${encodeURIComponent(id)}/request-delete${query}`);
|
||||
},
|
||||
};
|
||||
|
||||
export const publishJobsApi = {
|
||||
create(payload: CreatePublishJobRequest) {
|
||||
return apiClient.post<CreatePublishJobResponse, CreatePublishJobRequest>("/api/tenant/publish-jobs", payload);
|
||||
},
|
||||
};
|
||||
|
||||
function removedMediaApi<T>(action: string): Promise<T> {
|
||||
return Promise.reject(new ApiClientError({
|
||||
message: "desktop_media_api_removed",
|
||||
|
||||
@@ -50,6 +50,10 @@ const errorMessageMap: Record<string, string> = {
|
||||
brand_not_found: "品牌不存在或已删除",
|
||||
keyword_not_found: "关键词不存在或已删除",
|
||||
publish_cover_required: "已选择百家号,请先上传封面图",
|
||||
desktop_account_client_missing: "所选账号还没有绑定桌面客户端,暂时无法发布",
|
||||
desktop_account_not_found: "所选桌面账号不存在,请刷新后重试",
|
||||
desktop_publish_job_store_unavailable: "发布队列暂时不可用,请稍后重试",
|
||||
desktop_media_api_removed: "旧版媒体接口已下线,请刷新页面后重试",
|
||||
publisher_plugin_timeout: "浏览器插件响应超时,请刷新当前页面后重试",
|
||||
publisher_plugin_empty_response: "浏览器插件未返回数据,请刷新当前页面后重试",
|
||||
publisher_plugin_invalid_response: "浏览器插件返回了无效数据,请刷新当前页面后重试",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MediaPlatform, PlatformAccount } from "@geo/shared-types";
|
||||
import type { DesktopAccountInfo, MediaPlatform, PlatformAccount } from "@geo/shared-types";
|
||||
|
||||
export interface PublishPlatformOption {
|
||||
id: string;
|
||||
@@ -53,10 +53,14 @@ export function normalizePublishPlatformIds(platformIds?: Array<string | null |
|
||||
|
||||
export function buildPublishPlatformOptions(
|
||||
platforms: MediaPlatform[],
|
||||
accounts?: PlatformAccount[] | null,
|
||||
accounts?: Array<PlatformAccount | DesktopAccountInfo> | null,
|
||||
): PublishPlatformOption[] {
|
||||
const boundPlatformIds = new Set(
|
||||
(accounts ?? []).map((account) => normalizePublishPlatformId(account.platform_id)).filter(Boolean),
|
||||
(accounts ?? [])
|
||||
.map((account) =>
|
||||
normalizePublishPlatformId("platform_id" in account ? account.platform_id : account.platform),
|
||||
)
|
||||
.filter(Boolean),
|
||||
);
|
||||
|
||||
return platforms.map((platform) => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import ArticleEditorCanvas from "@/components/ArticleEditorCanvas.vue";
|
||||
import CoverPickerModal from "@/components/CoverPickerModal.vue";
|
||||
import PublishArticleModal from "@/components/PublishArticleModal.vue";
|
||||
import PublishPlatformSelector from "@/components/PublishPlatformSelector.vue";
|
||||
import { articlesApi, mediaApi, resolveApiURL } from "@/lib/api";
|
||||
import { articlesApi, mediaApi, resolveApiURL, tenantAccountsApi } from "@/lib/api";
|
||||
import {
|
||||
coverUploadRequired,
|
||||
deriveCoverFileName,
|
||||
@@ -55,8 +55,8 @@ const platformsQuery = useQuery({
|
||||
});
|
||||
|
||||
const accountsQuery = useQuery({
|
||||
queryKey: ["media", "platform-accounts", "article-editor"],
|
||||
queryFn: () => mediaApi.accounts(),
|
||||
queryKey: ["tenant", "desktop-accounts", "article-editor"],
|
||||
queryFn: () => tenantAccountsApi.list(),
|
||||
});
|
||||
|
||||
const publishPlatformOptions = computed(() =>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user