feat: enhance platform management and synchronization in ArticleEditor and PublishArticle components
This commit is contained in:
@@ -8,10 +8,11 @@ import { useI18n } from "vue-i18n";
|
|||||||
|
|
||||||
import PromptRuleModal from "@/components/PromptRuleModal.vue";
|
import PromptRuleModal from "@/components/PromptRuleModal.vue";
|
||||||
import PublishPlatformSelector from "@/components/PublishPlatformSelector.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 { formatError } from "@/lib/errors";
|
||||||
import {
|
import {
|
||||||
getAllPublishPlatforms,
|
buildPublishPlatformOptions,
|
||||||
|
normalizePublishPlatformIds,
|
||||||
parseTargetPlatforms,
|
parseTargetPlatforms,
|
||||||
serializeTargetPlatforms,
|
serializeTargetPlatforms,
|
||||||
} from "@/lib/publish-platforms";
|
} from "@/lib/publish-platforms";
|
||||||
@@ -44,7 +45,26 @@ const form = reactive({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isSchedule = computed(() => props.mode === "schedule");
|
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({
|
const rulesQuery = useQuery({
|
||||||
queryKey: ["promptRules", "simple"],
|
queryKey: ["promptRules", "simple"],
|
||||||
@@ -89,12 +109,7 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
const createScheduleMutation = useMutation({
|
const createScheduleMutation = useMutation({
|
||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
schedulesApi.create({
|
schedulesApi.create(buildSchedulePayload()),
|
||||||
name: form.name.trim(),
|
|
||||||
prompt_rule_id: form.promptRuleId!,
|
|
||||||
cron_expr: buildDailyCron(form.scheduleTime),
|
|
||||||
target_platform: serializeTargetPlatforms(form.platformIds),
|
|
||||||
}),
|
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
message.success(t("custom.messages.scheduleCreated"));
|
message.success(t("custom.messages.scheduleCreated"));
|
||||||
emit("update:open", false);
|
emit("update:open", false);
|
||||||
@@ -105,12 +120,7 @@ const createScheduleMutation = useMutation({
|
|||||||
|
|
||||||
const updateScheduleMutation = useMutation({
|
const updateScheduleMutation = useMutation({
|
||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
schedulesApi.update(props.task!.id, {
|
schedulesApi.update(props.task!.id, buildSchedulePayload()),
|
||||||
name: form.name.trim(),
|
|
||||||
prompt_rule_id: form.promptRuleId!,
|
|
||||||
cron_expr: buildDailyCron(form.scheduleTime),
|
|
||||||
target_platform: serializeTargetPlatforms(form.platformIds),
|
|
||||||
}),
|
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
message.success(t("custom.messages.scheduleUpdated"));
|
message.success(t("custom.messages.scheduleUpdated"));
|
||||||
emit("update:open", false);
|
emit("update:open", false);
|
||||||
@@ -121,18 +131,7 @@ const updateScheduleMutation = useMutation({
|
|||||||
|
|
||||||
const instantGenerateMutation = useMutation({
|
const instantGenerateMutation = useMutation({
|
||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
generateApi.fromRule({
|
generateApi.fromRule(buildInstantPayload()),
|
||||||
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,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
message.success(t("custom.messages.generateSubmitted"));
|
message.success(t("custom.messages.generateSubmitted"));
|
||||||
emit("update:open", false);
|
emit("update:open", false);
|
||||||
@@ -162,6 +161,36 @@ function hydrateForm(): void {
|
|||||||
resetCoverState();
|
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 {
|
function resetCoverState(): void {
|
||||||
if (coverPreviewUrl.value) {
|
if (coverPreviewUrl.value) {
|
||||||
URL.revokeObjectURL(coverPreviewUrl.value);
|
URL.revokeObjectURL(coverPreviewUrl.value);
|
||||||
@@ -323,7 +352,17 @@ function padTime(value: string): string {
|
|||||||
<div class="generate-task-drawer__field">
|
<div class="generate-task-drawer__field">
|
||||||
<label class="generate-task-drawer__label">{{ t("custom.task.platforms") }}:</label>
|
<label class="generate-task-drawer__label">{{ t("custom.task.platforms") }}:</label>
|
||||||
<p class="generate-task-drawer__hint">{{ t("custom.task.platformsHint") }}</p>
|
<p class="generate-task-drawer__hint">{{ t("custom.task.platformsHint") }}</p>
|
||||||
<PublishPlatformSelector v-model="form.platformIds" :platforms="allPlatforms" />
|
<a-skeleton
|
||||||
|
v-if="platformsLoading"
|
||||||
|
active
|
||||||
|
:title="false"
|
||||||
|
:paragraph="{ rows: 4 }"
|
||||||
|
/>
|
||||||
|
<PublishPlatformSelector
|
||||||
|
v-else
|
||||||
|
v-model="form.platformIds"
|
||||||
|
:platforms="allPlatforms"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="generate-task-drawer__field">
|
<div class="generate-task-drawer__field">
|
||||||
|
|||||||
@@ -9,11 +9,15 @@ import type {
|
|||||||
PublisherPublishResponse,
|
PublisherPublishResponse,
|
||||||
PublisherPublishTaskResult,
|
PublisherPublishTaskResult,
|
||||||
} from "@geo/shared-types";
|
} 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 { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
import { articlesApi, getApiBaseURL, mediaApi } from "@/lib/api";
|
import { articlesApi, getApiBaseURL, mediaApi } from "@/lib/api";
|
||||||
import { formatError } from "@/lib/errors";
|
import { formatError } from "@/lib/errors";
|
||||||
|
import {
|
||||||
|
normalizePublishPlatformId,
|
||||||
|
normalizePublishPlatformIds,
|
||||||
|
} from "@/lib/publish-platforms";
|
||||||
import { publishWithPublisherPlugin } from "@/lib/publisher-plugin";
|
import { publishWithPublisherPlugin } from "@/lib/publisher-plugin";
|
||||||
import { loadPublisherRuntimeState } from "@/lib/publisher-runtime";
|
import { loadPublisherRuntimeState } from "@/lib/publisher-runtime";
|
||||||
|
|
||||||
@@ -39,6 +43,7 @@ const pluginVersion = ref<string | undefined>();
|
|||||||
const pluginInstallationId = ref<number | null>(null);
|
const pluginInstallationId = ref<number | null>(null);
|
||||||
const localPlatforms = ref<PublisherLocalPlatformState[]>([]);
|
const localPlatforms = ref<PublisherLocalPlatformState[]>([]);
|
||||||
const fileList = ref<any[]>([]);
|
const fileList = ref<any[]>([]);
|
||||||
|
const selectionHydrated = ref(false);
|
||||||
|
|
||||||
function handleBeforeUpload(file: File) {
|
function handleBeforeUpload(file: File) {
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
@@ -84,27 +89,63 @@ watch(
|
|||||||
fileList.value = [];
|
fileList.value = [];
|
||||||
pluginInstallationId.value = null;
|
pluginInstallationId.value = null;
|
||||||
localPlatforms.value = [];
|
localPlatforms.value = [];
|
||||||
|
selectionHydrated.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
selectionHydrated.value = false;
|
||||||
await refreshRuntime();
|
await refreshRuntime();
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ 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(() => {
|
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<string, PlatformAccount[]>();
|
||||||
|
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(() => {
|
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 accountCards = computed(() => {
|
||||||
const accountMap = new Map((accountsQuery.data.value ?? []).map((account) => [account.platform_id, account]));
|
|
||||||
|
|
||||||
return (platformsQuery.data.value ?? []).map((platform) => {
|
return (platformsQuery.data.value ?? []).map((platform) => {
|
||||||
const account = accountMap.get(platform.platform_id) ?? null;
|
const platformId = normalizePublishPlatformId(platform.platform_id);
|
||||||
const local = localPlatformMap.value.get(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 localConnected = Boolean(local?.connected);
|
||||||
const uidMatches = account
|
const uidMatches = account
|
||||||
? localConnected && (!local?.platform_uid || local.platform_uid === account.platform_uid)
|
? 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);
|
const selectable = Boolean(account && account.status === "active" && uidMatches && pluginInstalled.value);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: account?.id ?? platform.platform_id,
|
key: account?.id ?? platformId,
|
||||||
accountId: account?.id ?? null,
|
accountId: account?.id ?? null,
|
||||||
platformId: platform.platform_id,
|
platformId,
|
||||||
platformName: platform.name,
|
platformName: platform.name,
|
||||||
platformShortName: platform.short_name,
|
platformShortName: platform.short_name,
|
||||||
platformAccentColor: platform.accent_color,
|
platformAccentColor: platform.accent_color,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { computed } from "vue";
|
|||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
import type { PublishPlatformOption } from "@/lib/publish-platforms";
|
import type { PublishPlatformOption } from "@/lib/publish-platforms";
|
||||||
import { isPlatformBound } from "@/lib/publish-platforms";
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
modelValue: string[];
|
modelValue: string[];
|
||||||
@@ -75,9 +74,9 @@ function togglePlatform(platformId: string): void {
|
|||||||
|
|
||||||
<span
|
<span
|
||||||
class="publish-platform-selector__status"
|
class="publish-platform-selector__status"
|
||||||
:class="{ 'publish-platform-selector__status--bound': isPlatformBound(platform.id) }"
|
:class="{ 'publish-platform-selector__status--bound': platform.bound }"
|
||||||
>
|
>
|
||||||
{{ 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") }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -401,7 +401,7 @@ const enUS = {
|
|||||||
saveAndLeave: "Save and leave",
|
saveAndLeave: "Save and leave",
|
||||||
},
|
},
|
||||||
platformsTitle: "Publish platforms",
|
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",
|
coverTitle: "Cover image",
|
||||||
coverHint: "Upload a cover asset before publishing. This is local preview only for now.",
|
coverHint: "Upload a cover asset before publishing. This is local preview only for now.",
|
||||||
coverUpload: "Upload cover image",
|
coverUpload: "Upload cover image",
|
||||||
@@ -516,7 +516,7 @@ const enUS = {
|
|||||||
subtitle: "Multi-platform Dispatch",
|
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.",
|
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",
|
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",
|
coverTitle: "Cover image",
|
||||||
coverHint: "This version uses a cover asset URL. Upload and crop can be wired later.",
|
coverHint: "This version uses a cover asset URL. Upload and crop can be wired later.",
|
||||||
coverPlaceholder: "Enter an optional cover asset URL",
|
coverPlaceholder: "Enter an optional cover asset URL",
|
||||||
@@ -646,10 +646,11 @@ const enUS = {
|
|||||||
promptPlaceholder: "Select a prompt",
|
promptPlaceholder: "Select a prompt",
|
||||||
createPrompt: "New Prompt",
|
createPrompt: "New Prompt",
|
||||||
platforms: "Publish platforms",
|
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",
|
platformConnected: "Connected",
|
||||||
platformEmptyTitle: "Publish platforms are coming",
|
platformUnauthorized: "Unauthorized",
|
||||||
platformEmptyHint: "Once the media integration is wired, bound tenant platforms will appear here automatically.",
|
platformEmptyTitle: "No publish platforms",
|
||||||
|
platformEmptyHint: "No platform data is available yet. Please verify the Media Management setup first.",
|
||||||
cover: "Cover image",
|
cover: "Cover image",
|
||||||
coverHint: "Keep the cover clear, complete, and visually polished.",
|
coverHint: "Keep the cover clear, complete, and visually polished.",
|
||||||
coverUpload: "Upload cover image",
|
coverUpload: "Upload cover image",
|
||||||
|
|||||||
@@ -408,7 +408,7 @@ const zhCN = {
|
|||||||
saveAndLeave: "保存并返回",
|
saveAndLeave: "保存并返回",
|
||||||
},
|
},
|
||||||
platformsTitle: "发布平台",
|
platformsTitle: "发布平台",
|
||||||
platformsHint: "这里会复用统一的发布平台组件,当前先展示前端占位的已绑定平台。",
|
platformsHint: "这里会同步媒体管理中的平台和绑定状态,支持直接编辑文章的发布平台。",
|
||||||
coverTitle: "封面图",
|
coverTitle: "封面图",
|
||||||
coverHint: "发布前可先上传封面素材,当前只做本地预览。",
|
coverHint: "发布前可先上传封面素材,当前只做本地预览。",
|
||||||
coverUpload: "上传封面图",
|
coverUpload: "上传封面图",
|
||||||
@@ -523,7 +523,7 @@ const zhCN = {
|
|||||||
subtitle: "Multi-platform Dispatch",
|
subtitle: "Multi-platform Dispatch",
|
||||||
description: "选择当前浏览器已登录且租户已绑定的平台,插件会逐个平台执行真实发布并回写链接。",
|
description: "选择当前浏览器已登录且租户已绑定的平台,插件会逐个平台执行真实发布并回写链接。",
|
||||||
platformsTitle: "发布平台",
|
platformsTitle: "发布平台",
|
||||||
platformsHint: "仅展示当前租户已绑定的账号。插件本地登录态异常的平台会自动禁用。",
|
platformsHint: "会优先回填文章里已选择且已绑定的账号,未绑定或本地登录态异常的平台不可选。",
|
||||||
coverTitle: "封面图",
|
coverTitle: "封面图",
|
||||||
coverHint: "当前版本先使用封面素材 URL,后续可以接入上传与裁剪。",
|
coverHint: "当前版本先使用封面素材 URL,后续可以接入上传与裁剪。",
|
||||||
coverPlaceholder: "请输入封面素材 URL(可选)",
|
coverPlaceholder: "请输入封面素材 URL(可选)",
|
||||||
@@ -654,11 +654,11 @@ const zhCN = {
|
|||||||
promptPlaceholder: "请选择Prompt",
|
promptPlaceholder: "请选择Prompt",
|
||||||
createPrompt: "新建Prompt",
|
createPrompt: "新建Prompt",
|
||||||
platforms: "发布平台",
|
platforms: "发布平台",
|
||||||
platformsHint: "请选择需要发布的平台,支持多选",
|
platformsHint: "同步媒体管理中的平台和绑定状态,可不选,支持多选",
|
||||||
platformConnected: "已绑定",
|
platformConnected: "已绑定",
|
||||||
platformUnauthorized: "未授权",
|
platformUnauthorized: "未授权",
|
||||||
platformEmptyTitle: "发布平台接入中",
|
platformEmptyTitle: "暂无发布平台",
|
||||||
platformEmptyHint: "媒体平台接口接入后,这里会自动展示当前租户已绑定的平台。",
|
platformEmptyHint: "当前还没有可用的平台数据,请先检查媒体管理配置。",
|
||||||
cover: "封面图",
|
cover: "封面图",
|
||||||
coverHint: "请保证封面清晰、美观和完整",
|
coverHint: "请保证封面清晰、美观和完整",
|
||||||
coverUpload: "上传封面图",
|
coverUpload: "上传封面图",
|
||||||
|
|||||||
@@ -1,44 +1,66 @@
|
|||||||
|
import type { MediaPlatform, PlatformAccount } from "@geo/shared-types";
|
||||||
|
|
||||||
export interface PublishPlatformOption {
|
export interface PublishPlatformOption {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
shortName: string;
|
shortName: string;
|
||||||
accent: string;
|
accent: string;
|
||||||
accountLabel?: string;
|
accountLabel?: string;
|
||||||
|
bound?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const platformCatalog: Record<string, Omit<PublishPlatformOption, "accountLabel">> = {
|
const platformCatalog: Record<string, Omit<PublishPlatformOption, "accountLabel" | "bound">> = {
|
||||||
zhihu: { id: "zhihu", name: "知乎", shortName: "知", accent: "#1677ff" },
|
toutiaohao: { id: "toutiaohao", name: "头条号", shortName: "头", accent: "#f5222d" },
|
||||||
baijiahao: { id: "baijiahao", name: "百家号", shortName: "百", accent: "#31445a" },
|
baijiahao: { id: "baijiahao", name: "百家号", shortName: "百", accent: "#31445a" },
|
||||||
toutiao: { id: "toutiao", name: "头条号", shortName: "头", accent: "#f5222d" },
|
sohuhao: { id: "sohuhao", name: "搜狐号", shortName: "搜", accent: "#fa8c16" },
|
||||||
souhu: { id: "souhu", name: "搜狐号", shortName: "搜", accent: "#fa8c16" },
|
qiehao: { id: "qiehao", name: "企鹅号", shortName: "Q", accent: "#111827" },
|
||||||
qq: { id: "qq", name: "企鹅号", shortName: "Q", accent: "#111827" },
|
zhihu: { id: "zhihu", name: "知乎", shortName: "知", accent: "#1677ff" },
|
||||||
wechat: { id: "wechat", name: "公众号", shortName: "微", accent: "#13c26b" },
|
wangyihao: { id: "wangyihao", name: "网易号", shortName: "网", accent: "#ff4d4f" },
|
||||||
bilibili: { id: "bilibili", name: "bilibili", shortName: "B", accent: "#eb2f96" },
|
|
||||||
jianshu: { id: "jianshu", name: "简书", shortName: "简", accent: "#f56a5e" },
|
jianshu: { id: "jianshu", name: "简书", shortName: "简", accent: "#f56a5e" },
|
||||||
|
bilibili: { id: "bilibili", name: "bilibili", shortName: "B", accent: "#eb2f96" },
|
||||||
juejin: { id: "juejin", name: "稀土掘金", shortName: "掘", accent: "#1677ff" },
|
juejin: { id: "juejin", name: "稀土掘金", shortName: "掘", accent: "#1677ff" },
|
||||||
smzdm: { id: "smzdm", name: "什么值得买", shortName: "值", accent: "#f5222d" },
|
smzdm: { id: "smzdm", name: "什么值得买", shortName: "值", accent: "#f5222d" },
|
||||||
|
weixin_gzh: { id: "weixin_gzh", name: "微信公众号", shortName: "微", accent: "#13c26b" },
|
||||||
zol: { id: "zol", name: "中关村在线", shortName: "Z", accent: "#ff4d4f" },
|
zol: { id: "zol", name: "中关村在线", shortName: "Z", accent: "#ff4d4f" },
|
||||||
dongchedi: { id: "dongchedi", name: "懂车帝", shortName: "懂", accent: "#fadb14" },
|
dongchedi: { id: "dongchedi", name: "懂车帝", shortName: "懂", accent: "#fadb14" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockBoundPlatformIds = ["zhihu", "wechat", "juejin", "bilibili", "jianshu", "baijiahao"];
|
export function normalizePublishPlatformId(platformId?: string | null): string {
|
||||||
|
return String(platformId ?? "").trim();
|
||||||
export function getBoundPublishPlatforms(): PublishPlatformOption[] {
|
|
||||||
return mockBoundPlatformIds
|
|
||||||
.map((id) => platformCatalog[id])
|
|
||||||
.filter((item): item is Omit<PublishPlatformOption, "accountLabel"> => Boolean(item));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllPublishPlatforms(): PublishPlatformOption[] {
|
export function normalizePublishPlatformIds(platformIds?: Array<string | null | undefined>): string[] {
|
||||||
return Object.values(platformCatalog);
|
return Array.from(
|
||||||
|
new Set(
|
||||||
|
(platformIds ?? [])
|
||||||
|
.map((item) => normalizePublishPlatformId(item))
|
||||||
|
.filter(Boolean),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPlatformBound(platformId: string): boolean {
|
export function buildPublishPlatformOptions(
|
||||||
return mockBoundPlatformIds.includes(platformId);
|
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 {
|
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;
|
return next.length ? next.join(",") : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,18 +69,12 @@ export function parseTargetPlatforms(value?: string | null): string[] {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(
|
return normalizePublishPlatformIds(value.split(","));
|
||||||
new Set(
|
|
||||||
value
|
|
||||||
.split(",")
|
|
||||||
.map((item) => item.trim())
|
|
||||||
.filter(Boolean),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPublishPlatformLabel(platformId: string): string {
|
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 {
|
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 {
|
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("、") : "--";
|
return labels.length ? labels.join("、") : "--";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,12 @@ import { MilkdownProvider } from "@milkdown/vue";
|
|||||||
import ArticleEditorCanvas from "@/components/ArticleEditorCanvas.vue";
|
import ArticleEditorCanvas from "@/components/ArticleEditorCanvas.vue";
|
||||||
import PublishArticleModal from "@/components/PublishArticleModal.vue";
|
import PublishArticleModal from "@/components/PublishArticleModal.vue";
|
||||||
import PublishPlatformSelector from "@/components/PublishPlatformSelector.vue";
|
import PublishPlatformSelector from "@/components/PublishPlatformSelector.vue";
|
||||||
import { articlesApi } from "@/lib/api";
|
import { articlesApi, mediaApi } from "@/lib/api";
|
||||||
import { formatError } from "@/lib/errors";
|
import { formatError } from "@/lib/errors";
|
||||||
import { getBoundPublishPlatforms } from "@/lib/publish-platforms";
|
import {
|
||||||
|
buildPublishPlatformOptions,
|
||||||
|
normalizePublishPlatformIds,
|
||||||
|
} from "@/lib/publish-platforms";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -24,7 +27,6 @@ const title = ref("");
|
|||||||
const markdown = ref("");
|
const markdown = ref("");
|
||||||
const initialTitle = ref("");
|
const initialTitle = ref("");
|
||||||
const initialMarkdown = ref("");
|
const initialMarkdown = ref("");
|
||||||
const boundPublishPlatforms = getBoundPublishPlatforms();
|
|
||||||
const publishPlatforms = ref<string[]>([]);
|
const publishPlatforms = ref<string[]>([]);
|
||||||
const initialPublishPlatforms = ref<string[]>([]);
|
const initialPublishPlatforms = ref<string[]>([]);
|
||||||
const coverEnabled = ref(true);
|
const coverEnabled = ref(true);
|
||||||
@@ -38,6 +40,24 @@ const detailQuery = useQuery({
|
|||||||
queryFn: () => articlesApi.detail(articleId.value),
|
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(
|
watch(
|
||||||
() => detailQuery.data.value,
|
() => detailQuery.data.value,
|
||||||
(detail) => {
|
(detail) => {
|
||||||
@@ -52,7 +72,7 @@ watch(
|
|||||||
markdown.value = nextMarkdown;
|
markdown.value = nextMarkdown;
|
||||||
initialTitle.value = nextTitle;
|
initialTitle.value = nextTitle;
|
||||||
initialMarkdown.value = nextMarkdown;
|
initialMarkdown.value = nextMarkdown;
|
||||||
publishPlatforms.value = [...(detail.platforms ?? [])];
|
publishPlatforms.value = normalizePublishPlatformIds(detail.platforms ?? []);
|
||||||
initialPublishPlatforms.value = [...publishPlatforms.value];
|
initialPublishPlatforms.value = [...publishPlatforms.value];
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
@@ -72,7 +92,7 @@ const saveMutation = useMutation({
|
|||||||
articlesApi.update(articleId.value, {
|
articlesApi.update(articleId.value, {
|
||||||
title: title.value.trim(),
|
title: title.value.trim(),
|
||||||
markdown_content: markdown.value,
|
markdown_content: markdown.value,
|
||||||
platforms: publishPlatforms.value,
|
platforms: normalizePublishPlatformIds(publishPlatforms.value),
|
||||||
}),
|
}),
|
||||||
onSuccess: async (data) => {
|
onSuccess: async (data) => {
|
||||||
message.success(t("article.editor.messages.saved"));
|
message.success(t("article.editor.messages.saved"));
|
||||||
@@ -80,7 +100,7 @@ const saveMutation = useMutation({
|
|||||||
markdown.value = data.markdown_content ?? "";
|
markdown.value = data.markdown_content ?? "";
|
||||||
initialTitle.value = title.value;
|
initialTitle.value = title.value;
|
||||||
initialMarkdown.value = markdown.value;
|
initialMarkdown.value = markdown.value;
|
||||||
publishPlatforms.value = [...(data.platforms ?? [])];
|
publishPlatforms.value = normalizePublishPlatformIds(data.platforms ?? []);
|
||||||
initialPublishPlatforms.value = [...publishPlatforms.value];
|
initialPublishPlatforms.value = [...publishPlatforms.value];
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
queryClient.invalidateQueries({ queryKey: ["articles"] }),
|
queryClient.invalidateQueries({ queryKey: ["articles"] }),
|
||||||
@@ -232,9 +252,7 @@ function stripLeadingTitleHeading(titleValue: string, markdownValue: string): st
|
|||||||
}
|
}
|
||||||
|
|
||||||
function serializePlatformSelection(platformIds: string[]): string {
|
function serializePlatformSelection(platformIds: string[]): string {
|
||||||
return [...platformIds]
|
return normalizePublishPlatformIds(platformIds)
|
||||||
.map((platformId) => platformId.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
.sort()
|
.sort()
|
||||||
.join(",");
|
.join(",");
|
||||||
}
|
}
|
||||||
@@ -287,10 +305,17 @@ function serializePlatformSelection(platformIds: string[]): string {
|
|||||||
<section class="article-editor-view__card">
|
<section class="article-editor-view__card">
|
||||||
<h3>{{ t("article.editor.platformsTitle") }}</h3>
|
<h3>{{ t("article.editor.platformsTitle") }}</h3>
|
||||||
<p>{{ t("article.editor.platformsHint") }}</p>
|
<p>{{ t("article.editor.platformsHint") }}</p>
|
||||||
|
<a-skeleton
|
||||||
|
v-if="publishPlatformsLoading"
|
||||||
|
active
|
||||||
|
:title="false"
|
||||||
|
:paragraph="{ rows: 4 }"
|
||||||
|
/>
|
||||||
<PublishPlatformSelector
|
<PublishPlatformSelector
|
||||||
|
v-else
|
||||||
v-model="publishPlatforms"
|
v-model="publishPlatforms"
|
||||||
class="article-editor-view__platforms"
|
class="article-editor-view__platforms"
|
||||||
:platforms="boundPublishPlatforms"
|
:platforms="publishPlatformOptions"
|
||||||
:disabled="editorLocked"
|
:disabled="editorLocked"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user