diff --git a/apps/admin-web/src/components/GenerateTaskDrawer.vue b/apps/admin-web/src/components/GenerateTaskDrawer.vue
index f9249b8..4ffe20d 100644
--- a/apps/admin-web/src/components/GenerateTaskDrawer.vue
+++ b/apps/admin-web/src/components/GenerateTaskDrawer.vue
@@ -8,10 +8,11 @@ import { useI18n } from "vue-i18n";
import PromptRuleModal from "@/components/PromptRuleModal.vue";
import PublishPlatformSelector from "@/components/PublishPlatformSelector.vue";
-import { generateApi, promptRulesApi, schedulesApi } from "@/lib/api";
+import { generateApi, mediaApi, promptRulesApi, schedulesApi } from "@/lib/api";
import { formatError } from "@/lib/errors";
import {
- getAllPublishPlatforms,
+ buildPublishPlatformOptions,
+ normalizePublishPlatformIds,
parseTargetPlatforms,
serializeTargetPlatforms,
} from "@/lib/publish-platforms";
@@ -44,7 +45,26 @@ const form = reactive({
});
const isSchedule = computed(() => props.mode === "schedule");
-const allPlatforms = computed(() => getAllPublishPlatforms());
+
+const platformsQuery = useQuery({
+ queryKey: ["media", "platforms", "task-drawer"],
+ enabled: computed(() => props.open),
+ queryFn: () => mediaApi.platforms(),
+});
+
+const accountsQuery = useQuery({
+ queryKey: ["media", "platform-accounts", "task-drawer"],
+ enabled: computed(() => props.open),
+ queryFn: () => mediaApi.accounts(),
+});
+
+const allPlatforms = computed(() =>
+ buildPublishPlatformOptions(platformsQuery.data.value ?? [], accountsQuery.data.value),
+);
+
+const platformsLoading = computed(
+ () => platformsQuery.isPending.value || accountsQuery.isPending.value,
+);
const rulesQuery = useQuery({
queryKey: ["promptRules", "simple"],
@@ -89,12 +109,7 @@ onBeforeUnmount(() => {
const createScheduleMutation = useMutation({
mutationFn: () =>
- schedulesApi.create({
- name: form.name.trim(),
- prompt_rule_id: form.promptRuleId!,
- cron_expr: buildDailyCron(form.scheduleTime),
- target_platform: serializeTargetPlatforms(form.platformIds),
- }),
+ schedulesApi.create(buildSchedulePayload()),
onSuccess: async () => {
message.success(t("custom.messages.scheduleCreated"));
emit("update:open", false);
@@ -105,12 +120,7 @@ const createScheduleMutation = useMutation({
const updateScheduleMutation = useMutation({
mutationFn: () =>
- schedulesApi.update(props.task!.id, {
- name: form.name.trim(),
- prompt_rule_id: form.promptRuleId!,
- cron_expr: buildDailyCron(form.scheduleTime),
- target_platform: serializeTargetPlatforms(form.platformIds),
- }),
+ schedulesApi.update(props.task!.id, buildSchedulePayload()),
onSuccess: async () => {
message.success(t("custom.messages.scheduleUpdated"));
emit("update:open", false);
@@ -121,18 +131,7 @@ const updateScheduleMutation = useMutation({
const instantGenerateMutation = useMutation({
mutationFn: () =>
- generateApi.fromRule({
- prompt_rule_id: form.promptRuleId!,
- target_platform: serializeTargetPlatforms(form.platformIds),
- extra_params: {
- task_name: form.name.trim(),
- generation_mode: "instant",
- enable_web_search: form.enableWebSearch,
- generate_count: form.generateCount,
- target_platforms: form.platformIds,
- cover_file_name: coverFileName.value || null,
- },
- }),
+ generateApi.fromRule(buildInstantPayload()),
onSuccess: async () => {
message.success(t("custom.messages.generateSubmitted"));
emit("update:open", false);
@@ -162,6 +161,36 @@ function hydrateForm(): void {
resetCoverState();
}
+function getSelectedPlatformIds(): string[] {
+ return normalizePublishPlatformIds(form.platformIds);
+}
+
+function buildSchedulePayload() {
+ return {
+ name: form.name.trim(),
+ prompt_rule_id: form.promptRuleId!,
+ cron_expr: buildDailyCron(form.scheduleTime),
+ target_platform: serializeTargetPlatforms(getSelectedPlatformIds()),
+ };
+}
+
+function buildInstantPayload() {
+ const platformIds = getSelectedPlatformIds();
+
+ return {
+ prompt_rule_id: form.promptRuleId!,
+ target_platform: serializeTargetPlatforms(platformIds),
+ extra_params: {
+ task_name: form.name.trim(),
+ generation_mode: "instant",
+ enable_web_search: form.enableWebSearch,
+ generate_count: form.generateCount,
+ target_platforms: platformIds,
+ cover_file_name: coverFileName.value || null,
+ },
+ };
+}
+
function resetCoverState(): void {
if (coverPreviewUrl.value) {
URL.revokeObjectURL(coverPreviewUrl.value);
@@ -323,7 +352,17 @@ function padTime(value: string): string {
{{ t("custom.task.platformsHint") }}
-
+
+
diff --git a/apps/admin-web/src/components/PublishArticleModal.vue b/apps/admin-web/src/components/PublishArticleModal.vue
index 485838f..3862216 100644
--- a/apps/admin-web/src/components/PublishArticleModal.vue
+++ b/apps/admin-web/src/components/PublishArticleModal.vue
@@ -9,11 +9,15 @@ import type {
PublisherPublishResponse,
PublisherPublishTaskResult,
} from "@geo/shared-types";
-import { computed, h, ref, watch } from "vue";
+import { computed, h, ref, watch, watchEffect } from "vue";
import { useI18n } from "vue-i18n";
import { articlesApi, getApiBaseURL, mediaApi } from "@/lib/api";
import { formatError } from "@/lib/errors";
+import {
+ normalizePublishPlatformId,
+ normalizePublishPlatformIds,
+} from "@/lib/publish-platforms";
import { publishWithPublisherPlugin } from "@/lib/publisher-plugin";
import { loadPublisherRuntimeState } from "@/lib/publisher-runtime";
@@ -39,6 +43,7 @@ const pluginVersion = ref
();
const pluginInstallationId = ref(null);
const localPlatforms = ref([]);
const fileList = ref([]);
+const selectionHydrated = ref(false);
function handleBeforeUpload(file: File) {
const url = URL.createObjectURL(file);
@@ -84,27 +89,63 @@ watch(
fileList.value = [];
pluginInstallationId.value = null;
localPlatforms.value = [];
+ selectionHydrated.value = false;
return;
}
+ selectionHydrated.value = false;
await refreshRuntime();
},
{ immediate: true },
);
+watchEffect(() => {
+ if (!props.open || selectionHydrated.value) {
+ return;
+ }
+
+ if (
+ detailQuery.isPending.value ||
+ accountsQuery.isPending.value ||
+ platformsQuery.isPending.value ||
+ runtimeLoading.value
+ ) {
+ return;
+ }
+
+ const selectedPlatforms = new Set(normalizePublishPlatformIds(detailQuery.data.value?.platforms ?? []));
+ selectedAccountIds.value = accountCards.value
+ .filter((account) => account.accountId && account.selectable && selectedPlatforms.has(account.platformId))
+ .map((account) => account.accountId as number);
+ selectionHydrated.value = true;
+});
+
const localPlatformMap = computed(() => {
- return new Map(localPlatforms.value.map((item) => [item.platform_id, item]));
+ return new Map(localPlatforms.value.map((item) => [normalizePublishPlatformId(item.platform_id), item]));
+});
+
+const accountGroups = computed(() => {
+ const groups = new Map();
+ for (const account of accountsQuery.data.value ?? []) {
+ const platformId = normalizePublishPlatformId(account.platform_id);
+ const list = groups.get(platformId) ?? [];
+ list.push(account);
+ groups.set(platformId, list);
+ }
+ return groups;
});
const platformNameMap = computed(() => {
- return new Map((platformsQuery.data.value ?? []).map((platform) => [platform.platform_id, platform.name]));
+ return new Map(
+ (platformsQuery.data.value ?? []).map((platform) => [normalizePublishPlatformId(platform.platform_id), platform.name]),
+ );
});
const accountCards = computed(() => {
- const accountMap = new Map((accountsQuery.data.value ?? []).map((account) => [account.platform_id, account]));
-
return (platformsQuery.data.value ?? []).map((platform) => {
- const account = accountMap.get(platform.platform_id) ?? null;
- const local = localPlatformMap.value.get(platform.platform_id);
+ const platformId = normalizePublishPlatformId(platform.platform_id);
+ const accounts = accountGroups.value.get(platformId) ?? [];
+ const account = accounts[0] ?? null;
+ const local = localPlatformMap.value.get(platformId);
const localConnected = Boolean(local?.connected);
const uidMatches = account
? localConnected && (!local?.platform_uid || local.platform_uid === account.platform_uid)
@@ -112,9 +153,9 @@ const accountCards = computed(() => {
const selectable = Boolean(account && account.status === "active" && uidMatches && pluginInstalled.value);
return {
- key: account?.id ?? platform.platform_id,
+ key: account?.id ?? platformId,
accountId: account?.id ?? null,
- platformId: platform.platform_id,
+ platformId,
platformName: platform.name,
platformShortName: platform.short_name,
platformAccentColor: platform.accent_color,
diff --git a/apps/admin-web/src/components/PublishPlatformSelector.vue b/apps/admin-web/src/components/PublishPlatformSelector.vue
index 90d81be..ef936f4 100644
--- a/apps/admin-web/src/components/PublishPlatformSelector.vue
+++ b/apps/admin-web/src/components/PublishPlatformSelector.vue
@@ -3,7 +3,6 @@ import { computed } from "vue";
import { useI18n } from "vue-i18n";
import type { PublishPlatformOption } from "@/lib/publish-platforms";
-import { isPlatformBound } from "@/lib/publish-platforms";
const props = withDefaults(defineProps<{
modelValue: string[];
@@ -75,9 +74,9 @@ function togglePlatform(platformId: string): void {
- {{ isPlatformBound(platform.id) ? (platform.accountLabel ?? t("custom.task.platformConnected")) : t("custom.task.platformUnauthorized") }}
+ {{ platform.bound ? (platform.accountLabel ?? t("custom.task.platformConnected")) : t("custom.task.platformUnauthorized") }}
diff --git a/apps/admin-web/src/i18n/messages/en-US.ts b/apps/admin-web/src/i18n/messages/en-US.ts
index 503e58c..46cb133 100644
--- a/apps/admin-web/src/i18n/messages/en-US.ts
+++ b/apps/admin-web/src/i18n/messages/en-US.ts
@@ -401,7 +401,7 @@ const enUS = {
saveAndLeave: "Save and leave",
},
platformsTitle: "Publish platforms",
- platformsHint: "This uses the shared publish-platform selector. For now it shows placeholder bound platforms on the frontend.",
+ platformsHint: "This stays in sync with Media Management so you can edit the article's publish platforms directly.",
coverTitle: "Cover image",
coverHint: "Upload a cover asset before publishing. This is local preview only for now.",
coverUpload: "Upload cover image",
@@ -516,7 +516,7 @@ const enUS = {
subtitle: "Multi-platform Dispatch",
description: "Choose tenant-bound accounts that are currently signed in locally. The extension executes the real publish flow and writes back the resulting links.",
platformsTitle: "Target platforms",
- platformsHint: "Only tenant-bound accounts are listed here. Accounts with missing or mismatched local sessions are disabled automatically.",
+ platformsHint: "Previously selected and bound accounts are prefilled first. Unbound or locally unavailable platforms stay disabled.",
coverTitle: "Cover image",
coverHint: "This version uses a cover asset URL. Upload and crop can be wired later.",
coverPlaceholder: "Enter an optional cover asset URL",
@@ -646,10 +646,11 @@ const enUS = {
promptPlaceholder: "Select a prompt",
createPrompt: "New Prompt",
platforms: "Publish platforms",
- platformsHint: "Choose one or more publish platforms. Only bound platforms are shown here.",
+ platformsHint: "Synced from Media Management. This field is optional and supports multiple selection.",
platformConnected: "Connected",
- platformEmptyTitle: "Publish platforms are coming",
- platformEmptyHint: "Once the media integration is wired, bound tenant platforms will appear here automatically.",
+ platformUnauthorized: "Unauthorized",
+ platformEmptyTitle: "No publish platforms",
+ platformEmptyHint: "No platform data is available yet. Please verify the Media Management setup first.",
cover: "Cover image",
coverHint: "Keep the cover clear, complete, and visually polished.",
coverUpload: "Upload cover image",
diff --git a/apps/admin-web/src/i18n/messages/zh-CN.ts b/apps/admin-web/src/i18n/messages/zh-CN.ts
index 55cc14f..f1b586a 100644
--- a/apps/admin-web/src/i18n/messages/zh-CN.ts
+++ b/apps/admin-web/src/i18n/messages/zh-CN.ts
@@ -408,7 +408,7 @@ const zhCN = {
saveAndLeave: "保存并返回",
},
platformsTitle: "发布平台",
- platformsHint: "这里会复用统一的发布平台组件,当前先展示前端占位的已绑定平台。",
+ platformsHint: "这里会同步媒体管理中的平台和绑定状态,支持直接编辑文章的发布平台。",
coverTitle: "封面图",
coverHint: "发布前可先上传封面素材,当前只做本地预览。",
coverUpload: "上传封面图",
@@ -523,7 +523,7 @@ const zhCN = {
subtitle: "Multi-platform Dispatch",
description: "选择当前浏览器已登录且租户已绑定的平台,插件会逐个平台执行真实发布并回写链接。",
platformsTitle: "发布平台",
- platformsHint: "仅展示当前租户已绑定的账号。插件本地登录态异常的平台会自动禁用。",
+ platformsHint: "会优先回填文章里已选择且已绑定的账号,未绑定或本地登录态异常的平台不可选。",
coverTitle: "封面图",
coverHint: "当前版本先使用封面素材 URL,后续可以接入上传与裁剪。",
coverPlaceholder: "请输入封面素材 URL(可选)",
@@ -654,11 +654,11 @@ const zhCN = {
promptPlaceholder: "请选择Prompt",
createPrompt: "新建Prompt",
platforms: "发布平台",
- platformsHint: "请选择需要发布的平台,支持多选",
+ platformsHint: "同步媒体管理中的平台和绑定状态,可不选,支持多选",
platformConnected: "已绑定",
platformUnauthorized: "未授权",
- platformEmptyTitle: "发布平台接入中",
- platformEmptyHint: "媒体平台接口接入后,这里会自动展示当前租户已绑定的平台。",
+ platformEmptyTitle: "暂无发布平台",
+ platformEmptyHint: "当前还没有可用的平台数据,请先检查媒体管理配置。",
cover: "封面图",
coverHint: "请保证封面清晰、美观和完整",
coverUpload: "上传封面图",
diff --git a/apps/admin-web/src/lib/publish-platforms.ts b/apps/admin-web/src/lib/publish-platforms.ts
index bda857d..e0e2ff5 100644
--- a/apps/admin-web/src/lib/publish-platforms.ts
+++ b/apps/admin-web/src/lib/publish-platforms.ts
@@ -1,44 +1,66 @@
+import type { MediaPlatform, PlatformAccount } from "@geo/shared-types";
+
export interface PublishPlatformOption {
id: string;
name: string;
shortName: string;
accent: string;
accountLabel?: string;
+ bound?: boolean;
}
-const platformCatalog: Record> = {
- zhihu: { id: "zhihu", name: "知乎", shortName: "知", accent: "#1677ff" },
+const platformCatalog: Record> = {
+ toutiaohao: { id: "toutiaohao", name: "头条号", shortName: "头", accent: "#f5222d" },
baijiahao: { id: "baijiahao", name: "百家号", shortName: "百", accent: "#31445a" },
- toutiao: { id: "toutiao", name: "头条号", shortName: "头", accent: "#f5222d" },
- souhu: { id: "souhu", name: "搜狐号", shortName: "搜", accent: "#fa8c16" },
- qq: { id: "qq", name: "企鹅号", shortName: "Q", accent: "#111827" },
- wechat: { id: "wechat", name: "公众号", shortName: "微", accent: "#13c26b" },
- bilibili: { id: "bilibili", name: "bilibili", shortName: "B", accent: "#eb2f96" },
+ sohuhao: { id: "sohuhao", name: "搜狐号", shortName: "搜", accent: "#fa8c16" },
+ qiehao: { id: "qiehao", name: "企鹅号", shortName: "Q", accent: "#111827" },
+ zhihu: { id: "zhihu", name: "知乎", shortName: "知", accent: "#1677ff" },
+ wangyihao: { id: "wangyihao", name: "网易号", shortName: "网", accent: "#ff4d4f" },
jianshu: { id: "jianshu", name: "简书", shortName: "简", accent: "#f56a5e" },
+ bilibili: { id: "bilibili", name: "bilibili", shortName: "B", accent: "#eb2f96" },
juejin: { id: "juejin", name: "稀土掘金", shortName: "掘", accent: "#1677ff" },
smzdm: { id: "smzdm", name: "什么值得买", shortName: "值", accent: "#f5222d" },
+ weixin_gzh: { id: "weixin_gzh", name: "微信公众号", shortName: "微", accent: "#13c26b" },
zol: { id: "zol", name: "中关村在线", shortName: "Z", accent: "#ff4d4f" },
dongchedi: { id: "dongchedi", name: "懂车帝", shortName: "懂", accent: "#fadb14" },
};
-const mockBoundPlatformIds = ["zhihu", "wechat", "juejin", "bilibili", "jianshu", "baijiahao"];
-
-export function getBoundPublishPlatforms(): PublishPlatformOption[] {
- return mockBoundPlatformIds
- .map((id) => platformCatalog[id])
- .filter((item): item is Omit => Boolean(item));
+export function normalizePublishPlatformId(platformId?: string | null): string {
+ return String(platformId ?? "").trim();
}
-export function getAllPublishPlatforms(): PublishPlatformOption[] {
- return Object.values(platformCatalog);
+export function normalizePublishPlatformIds(platformIds?: Array): string[] {
+ return Array.from(
+ new Set(
+ (platformIds ?? [])
+ .map((item) => normalizePublishPlatformId(item))
+ .filter(Boolean),
+ ),
+ );
}
-export function isPlatformBound(platformId: string): boolean {
- return mockBoundPlatformIds.includes(platformId);
+export function buildPublishPlatformOptions(
+ platforms: MediaPlatform[],
+ accounts?: PlatformAccount[] | null,
+): PublishPlatformOption[] {
+ const boundPlatformIds = new Set(
+ (accounts ?? []).map((account) => normalizePublishPlatformId(account.platform_id)).filter(Boolean),
+ );
+
+ return platforms.map((platform) => {
+ const id = normalizePublishPlatformId(platform.platform_id);
+ return {
+ id,
+ name: platform.name,
+ shortName: platform.short_name,
+ accent: platform.accent_color,
+ bound: boundPlatformIds.has(id),
+ };
+ });
}
export function serializeTargetPlatforms(platformIds: string[]): string | undefined {
- const next = Array.from(new Set(platformIds.map((item) => item.trim()).filter(Boolean)));
+ const next = normalizePublishPlatformIds(platformIds);
return next.length ? next.join(",") : undefined;
}
@@ -47,18 +69,12 @@ export function parseTargetPlatforms(value?: string | null): string[] {
return [];
}
- return Array.from(
- new Set(
- value
- .split(",")
- .map((item) => item.trim())
- .filter(Boolean),
- ),
- );
+ return normalizePublishPlatformIds(value.split(","));
}
export function getPublishPlatformLabel(platformId: string): string {
- return platformCatalog[platformId]?.name ?? platformId;
+ const normalized = normalizePublishPlatformId(platformId);
+ return platformCatalog[normalized]?.name ?? normalized;
}
export function formatPublishPlatformSummary(value?: string | null): string {
@@ -67,6 +83,6 @@ export function formatPublishPlatformSummary(value?: string | null): string {
}
export function formatPublishPlatformList(platformIds?: string[] | null): string {
- const labels = (platformIds ?? []).map(getPublishPlatformLabel).filter(Boolean);
+ const labels = normalizePublishPlatformIds(platformIds ?? []).map(getPublishPlatformLabel);
return labels.length ? labels.join("、") : "--";
}
diff --git a/apps/admin-web/src/views/ArticleEditorView.vue b/apps/admin-web/src/views/ArticleEditorView.vue
index 780bae1..6213c11 100644
--- a/apps/admin-web/src/views/ArticleEditorView.vue
+++ b/apps/admin-web/src/views/ArticleEditorView.vue
@@ -10,9 +10,12 @@ import { MilkdownProvider } from "@milkdown/vue";
import ArticleEditorCanvas from "@/components/ArticleEditorCanvas.vue";
import PublishArticleModal from "@/components/PublishArticleModal.vue";
import PublishPlatformSelector from "@/components/PublishPlatformSelector.vue";
-import { articlesApi } from "@/lib/api";
+import { articlesApi, mediaApi } from "@/lib/api";
import { formatError } from "@/lib/errors";
-import { getBoundPublishPlatforms } from "@/lib/publish-platforms";
+import {
+ buildPublishPlatformOptions,
+ normalizePublishPlatformIds,
+} from "@/lib/publish-platforms";
const route = useRoute();
const router = useRouter();
@@ -24,7 +27,6 @@ const title = ref("");
const markdown = ref("");
const initialTitle = ref("");
const initialMarkdown = ref("");
-const boundPublishPlatforms = getBoundPublishPlatforms();
const publishPlatforms = ref([]);
const initialPublishPlatforms = ref([]);
const coverEnabled = ref(true);
@@ -38,6 +40,24 @@ const detailQuery = useQuery({
queryFn: () => articlesApi.detail(articleId.value),
});
+const platformsQuery = useQuery({
+ queryKey: ["media", "platforms", "article-editor"],
+ queryFn: () => mediaApi.platforms(),
+});
+
+const accountsQuery = useQuery({
+ queryKey: ["media", "platform-accounts", "article-editor"],
+ queryFn: () => mediaApi.accounts(),
+});
+
+const publishPlatformOptions = computed(() =>
+ buildPublishPlatformOptions(platformsQuery.data.value ?? [], accountsQuery.data.value),
+);
+
+const publishPlatformsLoading = computed(
+ () => platformsQuery.isPending.value || accountsQuery.isPending.value,
+);
+
watch(
() => detailQuery.data.value,
(detail) => {
@@ -52,7 +72,7 @@ watch(
markdown.value = nextMarkdown;
initialTitle.value = nextTitle;
initialMarkdown.value = nextMarkdown;
- publishPlatforms.value = [...(detail.platforms ?? [])];
+ publishPlatforms.value = normalizePublishPlatformIds(detail.platforms ?? []);
initialPublishPlatforms.value = [...publishPlatforms.value];
},
{ immediate: true },
@@ -72,7 +92,7 @@ const saveMutation = useMutation({
articlesApi.update(articleId.value, {
title: title.value.trim(),
markdown_content: markdown.value,
- platforms: publishPlatforms.value,
+ platforms: normalizePublishPlatformIds(publishPlatforms.value),
}),
onSuccess: async (data) => {
message.success(t("article.editor.messages.saved"));
@@ -80,7 +100,7 @@ const saveMutation = useMutation({
markdown.value = data.markdown_content ?? "";
initialTitle.value = title.value;
initialMarkdown.value = markdown.value;
- publishPlatforms.value = [...(data.platforms ?? [])];
+ publishPlatforms.value = normalizePublishPlatformIds(data.platforms ?? []);
initialPublishPlatforms.value = [...publishPlatforms.value];
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["articles"] }),
@@ -232,9 +252,7 @@ function stripLeadingTitleHeading(titleValue: string, markdownValue: string): st
}
function serializePlatformSelection(platformIds: string[]): string {
- return [...platformIds]
- .map((platformId) => platformId.trim())
- .filter(Boolean)
+ return normalizePublishPlatformIds(platformIds)
.sort()
.join(",");
}
@@ -287,10 +305,17 @@ function serializePlatformSelection(platformIds: string[]): string {
{{ t("article.editor.platformsTitle") }}
{{ t("article.editor.platformsHint") }}
+