2026-04-03 00:39:15 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-04-13 16:08:12 +08:00
|
|
|
|
import { MinusCircleFilled, ReloadOutlined } from "@ant-design/icons-vue";
|
2026-04-03 00:39:15 +08:00
|
|
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query";
|
2026-04-03 17:48:30 +08:00
|
|
|
|
import { message, notification } from "ant-design-vue";
|
|
|
|
|
|
import type {
|
|
|
|
|
|
MediaPlatform,
|
|
|
|
|
|
PlatformAccount,
|
|
|
|
|
|
PublisherLocalPlatformState,
|
|
|
|
|
|
PublisherPublishResponse,
|
|
|
|
|
|
PublisherPublishTaskResult,
|
|
|
|
|
|
} from "@geo/shared-types";
|
2026-04-06 15:41:49 +08:00
|
|
|
|
import { computed, h, ref, watch, watchEffect } from "vue";
|
2026-04-03 00:39:15 +08:00
|
|
|
|
import { useI18n } from "vue-i18n";
|
|
|
|
|
|
|
2026-04-07 09:19:03 +08:00
|
|
|
|
import CoverPickerModal from "@/components/CoverPickerModal.vue";
|
|
|
|
|
|
import { articlesApi, getApiBaseURL, mediaApi, resolveApiURL } from "@/lib/api";
|
|
|
|
|
|
import {
|
|
|
|
|
|
coverUploadRequired,
|
|
|
|
|
|
deriveCoverFileName,
|
|
|
|
|
|
} from "@/lib/cover-requirements";
|
2026-04-03 00:39:15 +08:00
|
|
|
|
import { formatError } from "@/lib/errors";
|
2026-04-06 15:41:49 +08:00
|
|
|
|
import {
|
|
|
|
|
|
normalizePublishPlatformId,
|
|
|
|
|
|
normalizePublishPlatformIds,
|
|
|
|
|
|
} from "@/lib/publish-platforms";
|
2026-04-03 00:39:15 +08:00
|
|
|
|
import { publishWithPublisherPlugin } from "@/lib/publisher-plugin";
|
|
|
|
|
|
import { loadPublisherRuntimeState } from "@/lib/publisher-runtime";
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
open: boolean;
|
|
|
|
|
|
articleId: number | null;
|
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
"update:open": [value: boolean];
|
|
|
|
|
|
published: [];
|
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
const { t } = useI18n();
|
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
|
|
|
|
|
|
|
const selectedAccountIds = ref<number[]>([]);
|
2026-04-07 09:19:03 +08:00
|
|
|
|
const coverEnabled = ref(false);
|
2026-04-03 00:39:15 +08:00
|
|
|
|
const coverAssetUrl = ref("");
|
2026-04-07 09:19:03 +08:00
|
|
|
|
const coverFileName = ref("");
|
2026-04-03 00:39:15 +08:00
|
|
|
|
const runtimeLoading = ref(false);
|
|
|
|
|
|
const pluginInstalled = ref(false);
|
|
|
|
|
|
const pluginVersion = ref<string | undefined>();
|
|
|
|
|
|
const pluginInstallationId = ref<number | null>(null);
|
|
|
|
|
|
const localPlatforms = ref<PublisherLocalPlatformState[]>([]);
|
2026-04-07 09:19:03 +08:00
|
|
|
|
const coverPickerOpen = ref(false);
|
2026-04-06 15:41:49 +08:00
|
|
|
|
const selectionHydrated = ref(false);
|
2026-04-07 09:19:03 +08:00
|
|
|
|
const coverHydrated = ref(false);
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
|
|
|
|
|
const detailQuery = useQuery({
|
|
|
|
|
|
queryKey: computed(() => ["articles", "detail", props.articleId, "publish-modal"]),
|
|
|
|
|
|
enabled: computed(() => props.open && Boolean(props.articleId)),
|
|
|
|
|
|
queryFn: () => articlesApi.detail(props.articleId as number),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const accountsQuery = useQuery({
|
|
|
|
|
|
queryKey: ["media", "platform-accounts", "publish-modal"],
|
|
|
|
|
|
enabled: computed(() => props.open),
|
|
|
|
|
|
queryFn: () => mediaApi.accounts(),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const platformsQuery = useQuery({
|
|
|
|
|
|
queryKey: ["media", "platforms", "publish-modal"],
|
|
|
|
|
|
enabled: computed(() => props.open),
|
|
|
|
|
|
queryFn: () => mediaApi.platforms(),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.open,
|
|
|
|
|
|
async (open) => {
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
selectedAccountIds.value = [];
|
|
|
|
|
|
coverAssetUrl.value = "";
|
2026-04-07 09:19:03 +08:00
|
|
|
|
coverFileName.value = "";
|
|
|
|
|
|
coverEnabled.value = false;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
pluginInstallationId.value = null;
|
|
|
|
|
|
localPlatforms.value = [];
|
2026-04-06 15:41:49 +08:00
|
|
|
|
selectionHydrated.value = false;
|
2026-04-07 09:19:03 +08:00
|
|
|
|
coverHydrated.value = false;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-06 15:41:49 +08:00
|
|
|
|
selectionHydrated.value = false;
|
2026-04-07 09:19:03 +08:00
|
|
|
|
coverHydrated.value = false;
|
2026-04-13 16:08:12 +08:00
|
|
|
|
await Promise.allSettled([
|
|
|
|
|
|
refreshRuntime(),
|
|
|
|
|
|
props.articleId ? detailQuery.refetch() : Promise.resolve(),
|
|
|
|
|
|
accountsQuery.refetch(),
|
|
|
|
|
|
platformsQuery.refetch(),
|
|
|
|
|
|
]);
|
2026-04-03 00:39:15 +08:00
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true },
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-04-06 15:41:49 +08:00
|
|
|
|
watchEffect(() => {
|
2026-04-07 09:19:03 +08:00
|
|
|
|
if (!props.open) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 16:08:12 +08:00
|
|
|
|
if (!coverHydrated.value) {
|
|
|
|
|
|
if (detailQuery.isPending.value || detailQuery.isFetching.value) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-07 09:19:03 +08:00
|
|
|
|
const initialUrl = resolveApiURL(detailQuery.data.value?.cover_asset_url);
|
|
|
|
|
|
coverAssetUrl.value = initialUrl;
|
|
|
|
|
|
coverFileName.value = deriveCoverFileName(initialUrl);
|
|
|
|
|
|
coverEnabled.value = Boolean(initialUrl);
|
|
|
|
|
|
coverHydrated.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (selectionHydrated.value) {
|
2026-04-06 15:41:49 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
detailQuery.isPending.value ||
|
2026-04-13 16:08:12 +08:00
|
|
|
|
detailQuery.isFetching.value ||
|
2026-04-06 15:41:49 +08:00
|
|
|
|
accountsQuery.isPending.value ||
|
2026-04-13 16:08:12 +08:00
|
|
|
|
accountsQuery.isFetching.value ||
|
2026-04-06 15:41:49 +08:00
|
|
|
|
platformsQuery.isPending.value ||
|
2026-04-13 16:08:12 +08:00
|
|
|
|
platformsQuery.isFetching.value ||
|
2026-04-06 15:41:49 +08:00
|
|
|
|
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;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-03 00:39:15 +08:00
|
|
|
|
const localPlatformMap = computed(() => {
|
2026-04-06 15:41:49 +08:00
|
|
|
|
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;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-03 17:48:30 +08:00
|
|
|
|
const platformNameMap = computed(() => {
|
2026-04-06 15:41:49 +08:00
|
|
|
|
return new Map(
|
|
|
|
|
|
(platformsQuery.data.value ?? []).map((platform) => [normalizePublishPlatformId(platform.platform_id), platform.name]),
|
|
|
|
|
|
);
|
2026-04-03 17:48:30 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-03 00:39:15 +08:00
|
|
|
|
const accountCards = computed(() => {
|
|
|
|
|
|
return (platformsQuery.data.value ?? []).map((platform) => {
|
2026-04-06 15:41:49 +08:00
|
|
|
|
const platformId = normalizePublishPlatformId(platform.platform_id);
|
|
|
|
|
|
const accounts = accountGroups.value.get(platformId) ?? [];
|
|
|
|
|
|
const account = accounts[0] ?? null;
|
|
|
|
|
|
const local = localPlatformMap.value.get(platformId);
|
2026-04-03 00:39:15 +08:00
|
|
|
|
const localConnected = Boolean(local?.connected);
|
|
|
|
|
|
const uidMatches = account
|
|
|
|
|
|
? localConnected && (!local?.platform_uid || local.platform_uid === account.platform_uid)
|
|
|
|
|
|
: false;
|
|
|
|
|
|
const selectable = Boolean(account && account.status === "active" && uidMatches && pluginInstalled.value);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
2026-04-06 15:41:49 +08:00
|
|
|
|
key: account?.id ?? platformId,
|
2026-04-03 00:39:15 +08:00
|
|
|
|
accountId: account?.id ?? null,
|
2026-04-06 15:41:49 +08:00
|
|
|
|
platformId,
|
2026-04-03 00:39:15 +08:00
|
|
|
|
platformName: platform.name,
|
2026-04-03 17:48:30 +08:00
|
|
|
|
platformShortName: platform.short_name,
|
|
|
|
|
|
platformAccentColor: platform.accent_color,
|
2026-04-03 00:39:15 +08:00
|
|
|
|
platformCategory: platform.category,
|
|
|
|
|
|
platformUid: account?.platform_uid ?? local?.platform_uid ?? "--",
|
|
|
|
|
|
nickname: account?.nickname ?? local?.nickname ?? t("media.card.unbound"),
|
|
|
|
|
|
status: account?.status ?? "unbound",
|
|
|
|
|
|
local,
|
|
|
|
|
|
selectable,
|
|
|
|
|
|
statusText: resolveAccountStatusText(platform, account, local),
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const modalTitle = computed(() => detailQuery.data.value?.title || t("article.untitled"));
|
2026-04-07 09:19:03 +08:00
|
|
|
|
const selectedPlatformIds = computed(() =>
|
|
|
|
|
|
accountCards.value
|
|
|
|
|
|
.filter((account) => account.accountId && isSelected(account.accountId))
|
|
|
|
|
|
.map((account) => account.platformId),
|
|
|
|
|
|
);
|
|
|
|
|
|
const publishModalVisible = computed(() => props.open && !coverPickerOpen.value);
|
|
|
|
|
|
const coverRequired = computed(() => coverUploadRequired(selectedPlatformIds.value));
|
|
|
|
|
|
const effectiveCoverEnabled = computed(() => coverRequired.value || coverEnabled.value);
|
|
|
|
|
|
const normalizedCoverValue = computed(() => (effectiveCoverEnabled.value ? coverAssetUrl.value.trim() : ""));
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
coverRequired,
|
|
|
|
|
|
(required) => {
|
|
|
|
|
|
if (required) {
|
|
|
|
|
|
coverEnabled.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true },
|
|
|
|
|
|
);
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
|
|
|
|
|
const publishMutation = useMutation({
|
|
|
|
|
|
mutationFn: async () => {
|
|
|
|
|
|
if (!props.articleId || !detailQuery.data.value) {
|
|
|
|
|
|
throw new Error("missing_article");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!selectedAccountIds.value.length) {
|
|
|
|
|
|
throw new Error("no_accounts_selected");
|
|
|
|
|
|
}
|
2026-04-07 09:19:03 +08:00
|
|
|
|
if (coverRequired.value && !normalizedCoverValue.value) {
|
|
|
|
|
|
throw new Error("cover_required_for_selected_platforms");
|
|
|
|
|
|
}
|
2026-04-03 00:39:15 +08:00
|
|
|
|
|
|
|
|
|
|
const batch = await articlesApi.publishBatch(props.articleId, {
|
|
|
|
|
|
platform_account_ids: selectedAccountIds.value,
|
|
|
|
|
|
plugin_installation_id: pluginInstallationId.value,
|
|
|
|
|
|
publish_type: "publish",
|
2026-04-07 09:19:03 +08:00
|
|
|
|
cover_asset_url: normalizedCoverValue.value || null,
|
2026-04-03 00:39:15 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return publishWithPublisherPlugin({
|
|
|
|
|
|
article_id: detailQuery.data.value.id,
|
|
|
|
|
|
title: detailQuery.data.value.title ?? t("article.untitled"),
|
|
|
|
|
|
markdown_content: detailQuery.data.value.markdown_content ?? "",
|
|
|
|
|
|
html_content: detailQuery.data.value.html_content ?? null,
|
2026-04-07 09:19:03 +08:00
|
|
|
|
cover_asset_url: normalizedCoverValue.value || null,
|
2026-04-03 00:39:15 +08:00
|
|
|
|
callback_base_url: getApiBaseURL(),
|
|
|
|
|
|
publish_type: "publish",
|
|
|
|
|
|
tasks: batch.tasks,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
onSuccess: async (result) => {
|
|
|
|
|
|
const successCount = result.results.filter((item) => item.success).length;
|
|
|
|
|
|
const failedCount = result.results.length - successCount;
|
|
|
|
|
|
if (failedCount === 0) {
|
2026-04-03 17:48:30 +08:00
|
|
|
|
notification.success({
|
|
|
|
|
|
message: t("media.publish.messages.successTitle"),
|
|
|
|
|
|
description: t("media.publish.messages.success", { count: successCount }),
|
|
|
|
|
|
placement: "topRight",
|
|
|
|
|
|
duration: 4.5,
|
|
|
|
|
|
});
|
2026-04-03 00:39:15 +08:00
|
|
|
|
} else {
|
2026-04-03 17:48:30 +08:00
|
|
|
|
showPublishFailures(result);
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["articles"] }),
|
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["workspace"] }),
|
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["media"] }),
|
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["articles", "detail", props.articleId] }),
|
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["articles", "publish-records", props.articleId] }),
|
|
|
|
|
|
]);
|
|
|
|
|
|
emit("published");
|
|
|
|
|
|
emit("update:open", false);
|
|
|
|
|
|
},
|
|
|
|
|
|
onError: (error) => {
|
|
|
|
|
|
const normalized = error instanceof Error ? error.message : "";
|
|
|
|
|
|
if (normalized === "no_accounts_selected") {
|
|
|
|
|
|
message.warning(t("media.publish.messages.selectPlatform"));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-07 09:19:03 +08:00
|
|
|
|
if (normalized === "cover_required_for_selected_platforms") {
|
|
|
|
|
|
message.warning(t("media.publish.messages.coverRequired"));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-03 00:39:15 +08:00
|
|
|
|
message.error(formatError(error));
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
async function refreshRuntime(): Promise<void> {
|
|
|
|
|
|
runtimeLoading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const runtime = await loadPublisherRuntimeState();
|
|
|
|
|
|
pluginInstalled.value = runtime.ping.installed;
|
|
|
|
|
|
pluginVersion.value = runtime.ping.version;
|
|
|
|
|
|
pluginInstallationId.value = runtime.pluginInstallationId;
|
|
|
|
|
|
localPlatforms.value = runtime.localPlatforms;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
pluginInstalled.value = false;
|
|
|
|
|
|
localPlatforms.value = [];
|
|
|
|
|
|
message.error(formatError(error));
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
runtimeLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleAccount(accountId: number, selectable: boolean): void {
|
|
|
|
|
|
if (!selectable || publishMutation.isPending.value) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (selectedAccountIds.value.includes(accountId)) {
|
|
|
|
|
|
selectedAccountIds.value = selectedAccountIds.value.filter((id) => id !== accountId);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
selectedAccountIds.value = [...selectedAccountIds.value, accountId];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isSelected(accountId: number): boolean {
|
|
|
|
|
|
return selectedAccountIds.value.includes(accountId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 09:19:03 +08:00
|
|
|
|
function handleCoverToggle(checked: boolean): void {
|
|
|
|
|
|
if (coverRequired.value) {
|
|
|
|
|
|
coverEnabled.value = true;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
coverEnabled.value = checked;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleCoverPicked(payload: { url: string; fileName: string }): void {
|
|
|
|
|
|
coverAssetUrl.value = payload.url;
|
|
|
|
|
|
coverFileName.value = payload.fileName;
|
|
|
|
|
|
coverEnabled.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleRemoveCover(): void {
|
|
|
|
|
|
coverAssetUrl.value = "";
|
|
|
|
|
|
coverFileName.value = "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 00:39:15 +08:00
|
|
|
|
function resolveAccountStatusText(
|
|
|
|
|
|
platform: MediaPlatform,
|
|
|
|
|
|
account: PlatformAccount | null,
|
|
|
|
|
|
local?: PublisherLocalPlatformState,
|
|
|
|
|
|
): string {
|
|
|
|
|
|
if (!account) {
|
|
|
|
|
|
return t("media.card.unboundHint", { platform: platform.name });
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!pluginInstalled.value) {
|
|
|
|
|
|
return t("media.plugin.notInstalled");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!local?.connected) {
|
|
|
|
|
|
return t("media.account.localMissing");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (local.platform_uid && local.platform_uid !== account.platform_uid) {
|
|
|
|
|
|
return t("media.account.localMismatch");
|
|
|
|
|
|
}
|
|
|
|
|
|
return t("media.account.ready");
|
|
|
|
|
|
}
|
2026-04-03 17:48:30 +08:00
|
|
|
|
|
|
|
|
|
|
function platformNameForResult(result: PublisherPublishTaskResult): string {
|
|
|
|
|
|
return platformNameMap.value.get(result.platform_id) ?? result.platform_id;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function publishFailureText(result: PublisherPublishTaskResult): string {
|
|
|
|
|
|
return t("media.publish.messages.failureItem", {
|
|
|
|
|
|
platform: platformNameForResult(result),
|
|
|
|
|
|
reason: result.message?.trim() || t("media.publish.messages.unknownFailure"),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function showPublishFailures(result: PublisherPublishResponse): void {
|
|
|
|
|
|
const failedResults = result.results.filter((item) => !item.success);
|
|
|
|
|
|
if (!failedResults.length) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const successCount = result.results.filter((item) => item.success).length;
|
|
|
|
|
|
const failedCount = failedResults.length;
|
|
|
|
|
|
const summary = t("media.publish.messages.partial", {
|
|
|
|
|
|
success: successCount,
|
|
|
|
|
|
failed: failedCount,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
notification.error({
|
|
|
|
|
|
message: t("media.publish.messages.failureTitle"),
|
|
|
|
|
|
description: h(
|
|
|
|
|
|
"div",
|
|
|
|
|
|
[
|
|
|
|
|
|
h("div", { style: "line-height:1.75;font-weight:500;margin-bottom:8px;" }, summary),
|
|
|
|
|
|
...failedResults.map((item) =>
|
|
|
|
|
|
h("div", { style: "line-height:1.75;" }, publishFailureText(item)),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
placement: "topRight",
|
|
|
|
|
|
duration: 8,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-04-03 00:39:15 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<a-modal
|
2026-04-07 09:19:03 +08:00
|
|
|
|
:open="publishModalVisible"
|
2026-04-03 00:39:15 +08:00
|
|
|
|
:title="t('media.publish.title')"
|
2026-04-07 09:19:03 +08:00
|
|
|
|
:width="920"
|
2026-04-03 00:39:15 +08:00
|
|
|
|
:confirm-loading="publishMutation.isPending.value"
|
|
|
|
|
|
@ok="publishMutation.mutate()"
|
|
|
|
|
|
@cancel="emit('update:open', false)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="publish-modal">
|
|
|
|
|
|
<section class="publish-modal__hero">
|
2026-04-03 17:48:30 +08:00
|
|
|
|
<div class="publish-modal__hero-top">
|
|
|
|
|
|
<span class="eyebrow">标题</span>
|
|
|
|
|
|
<div class="publish-modal__hero-actions">
|
|
|
|
|
|
<a-tag :color="pluginInstalled ? 'success' : 'error'" class="hero-tag">
|
|
|
|
|
|
{{ pluginInstalled ? t("media.plugin.ready") : t("media.plugin.notInstalled") }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
<a-button :loading="runtimeLoading" @click="refreshRuntime" size="small" class="hero-btn">
|
|
|
|
|
|
<template #icon><ReloadOutlined /></template>
|
|
|
|
|
|
{{ t("media.actions.redetect") }}
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
2026-04-03 00:39:15 +08:00
|
|
|
|
</div>
|
2026-04-03 17:48:30 +08:00
|
|
|
|
<h3>{{ modalTitle }}</h3>
|
2026-04-03 00:39:15 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<a-alert
|
|
|
|
|
|
v-if="!pluginInstalled"
|
|
|
|
|
|
type="warning"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
:message="t('media.plugin.installTitle')"
|
|
|
|
|
|
:description="t('media.plugin.installDesc')"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="publish-modal__section">
|
2026-04-03 17:48:30 +08:00
|
|
|
|
<div class="publish-modal__section-header">
|
|
|
|
|
|
<h3><span class="required-star">*</span> {{ t("media.publish.platformsTitle") }}:</h3>
|
|
|
|
|
|
<p class="muted">{{ t("media.publish.platformsHint") }}</p>
|
2026-04-03 00:39:15 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="accountsQuery.isPending.value || platformsQuery.isPending.value" class="publish-modal__loading">
|
|
|
|
|
|
<a-skeleton active :paragraph="{ rows: 5 }" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else-if="accountCards.length" class="publish-modal__grid">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="account in accountCards"
|
|
|
|
|
|
:key="account.key"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="publish-modal__card"
|
|
|
|
|
|
:class="{
|
|
|
|
|
|
'publish-modal__card--active': account.accountId ? isSelected(account.accountId) : false,
|
|
|
|
|
|
'publish-modal__card--disabled': !account.selectable,
|
|
|
|
|
|
}"
|
|
|
|
|
|
@click="account.accountId ? toggleAccount(account.accountId, account.selectable) : undefined"
|
|
|
|
|
|
>
|
2026-04-03 17:48:30 +08:00
|
|
|
|
<div class="publish-modal__card-left">
|
|
|
|
|
|
<span class="publish-modal__check">
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-if="account.accountId && isSelected(account.accountId)"
|
|
|
|
|
|
class="publish-modal__check-inner"
|
|
|
|
|
|
></span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="publish-modal__card-badge" :style="{ color: account.platformAccentColor }">
|
|
|
|
|
|
{{ account.platformShortName }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<div class="publish-modal__card-text">
|
|
|
|
|
|
<span class="publish-modal__card-name">{{ account.platformName }}</span>
|
|
|
|
|
|
<span class="publish-modal__card-sub" v-if="account.accountId">
|
|
|
|
|
|
{{ account.nickname }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span class="publish-modal__card-sub" v-else>
|
|
|
|
|
|
{{ t("media.card.unbound") }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="publish-modal__card-right">
|
|
|
|
|
|
<span class="publish-modal__card-status" :class="account.selectable ? 'status-selectable' : 'status-disabled'">
|
|
|
|
|
|
{{ account.selectable ? t("media.account.selectable") : t("media.account.unavailable") }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2026-04-03 00:39:15 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<a-empty v-else :description="t('media.empty.accounts')" />
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="publish-modal__section">
|
2026-04-03 17:48:30 +08:00
|
|
|
|
<div class="publish-modal__section-header inline-header">
|
|
|
|
|
|
<h3>{{ t("media.publish.coverTitle") }}:</h3>
|
2026-04-07 09:19:03 +08:00
|
|
|
|
<a-switch
|
|
|
|
|
|
:checked="effectiveCoverEnabled"
|
|
|
|
|
|
:disabled="coverRequired"
|
|
|
|
|
|
@change="handleCoverToggle"
|
|
|
|
|
|
/>
|
2026-04-03 17:48:30 +08:00
|
|
|
|
</div>
|
2026-04-07 09:19:03 +08:00
|
|
|
|
<p class="muted">
|
|
|
|
|
|
{{ coverRequired ? t("media.publish.messages.coverRequired") : t("media.publish.coverHint") }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
2026-04-13 16:08:12 +08:00
|
|
|
|
<div v-if="effectiveCoverEnabled" class="publish-modal__cover-body">
|
|
|
|
|
|
<div class="publish-modal__cover-preview-wrap">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="publish-modal__cover-preview"
|
|
|
|
|
|
@click="coverPickerOpen = true"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template v-if="coverAssetUrl">
|
|
|
|
|
|
<img :src="coverAssetUrl" alt="cover preview" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<span class="publish-modal__cover-plus">+</span>
|
|
|
|
|
|
<span>{{ t("media.publish.coverUpload") }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="coverAssetUrl"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
class="publish-modal__cover-remove"
|
|
|
|
|
|
:aria-label="t('article.editor.coverRemove')"
|
|
|
|
|
|
:title="t('article.editor.coverRemove')"
|
|
|
|
|
|
@click.stop="handleRemoveCover"
|
|
|
|
|
|
>
|
|
|
|
|
|
<MinusCircleFilled />
|
|
|
|
|
|
</button>
|
2026-04-07 09:19:03 +08:00
|
|
|
|
</div>
|
2026-04-03 00:39:15 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
2026-04-07 09:19:03 +08:00
|
|
|
|
|
2026-04-03 00:39:15 +08:00
|
|
|
|
</a-modal>
|
2026-04-07 09:19:03 +08:00
|
|
|
|
|
|
|
|
|
|
<CoverPickerModal
|
|
|
|
|
|
v-model:open="coverPickerOpen"
|
|
|
|
|
|
:article-id="detailQuery.data.value?.id ?? null"
|
|
|
|
|
|
:platform-ids="selectedPlatformIds"
|
|
|
|
|
|
:current-url="coverAssetUrl"
|
|
|
|
|
|
:current-file-name="coverFileName"
|
|
|
|
|
|
@confirmed="handleCoverPicked"
|
|
|
|
|
|
/>
|
2026-04-03 00:39:15 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.publish-modal {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-04-03 17:48:30 +08:00
|
|
|
|
gap: 24px;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-modal__hero {
|
|
|
|
|
|
display: flex;
|
2026-04-03 17:48:30 +08:00
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
padding: 16px 20px;
|
|
|
|
|
|
border: 1px solid #e5e5e5;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
background: #f7f7f7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-modal__hero-top {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 16px;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
justify-content: space-between;
|
2026-04-03 17:48:30 +08:00
|
|
|
|
margin-bottom: 12px;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 17:48:30 +08:00
|
|
|
|
.publish-modal__hero h3 {
|
2026-04-03 00:39:15 +08:00
|
|
|
|
margin: 0;
|
2026-04-03 17:48:30 +08:00
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #262626;
|
|
|
|
|
|
line-height: 1.5;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 17:48:30 +08:00
|
|
|
|
.eyebrow {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
color: #000000;
|
|
|
|
|
|
margin: 0;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-modal__hero-actions {
|
|
|
|
|
|
display: flex;
|
2026-04-03 17:48:30 +08:00
|
|
|
|
align-items: center;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 17:48:30 +08:00
|
|
|
|
.hero-tag {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 00:39:15 +08:00
|
|
|
|
.publish-modal__section {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-04-03 17:48:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-modal__section-header {
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-modal__section-header.inline-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-modal__section-header h3 {
|
|
|
|
|
|
margin: 0 0 6px 0;
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #262626;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-modal__section-header.inline-header h3 {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.required-star {
|
|
|
|
|
|
color: #ff4d4f;
|
|
|
|
|
|
margin-right: 4px;
|
|
|
|
|
|
font-family: SimSun, sans-serif;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.muted {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #8c8c8c;
|
|
|
|
|
|
margin: 0;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-modal__grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
2026-04-03 17:48:30 +08:00
|
|
|
|
gap: 12px;
|
|
|
|
|
|
margin-top: 4px;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-modal__card {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-04-03 17:48:30 +08:00
|
|
|
|
justify-content: space-between;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
width: 100%;
|
2026-04-03 17:48:30 +08:00
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
border: 1px solid #d9d9d9;
|
|
|
|
|
|
border-radius: 8px;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
background: #fff;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
cursor: pointer;
|
2026-04-03 17:48:30 +08:00
|
|
|
|
transition: all 0.2s ease;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 17:48:30 +08:00
|
|
|
|
.publish-modal__card:hover:not(.publish-modal__card--disabled) {
|
|
|
|
|
|
border-color: #1677ff;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-modal__card--active {
|
|
|
|
|
|
border-color: #1677ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-modal__card--disabled {
|
2026-04-03 17:48:30 +08:00
|
|
|
|
opacity: 0.6;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
cursor: not-allowed;
|
2026-04-03 17:48:30 +08:00
|
|
|
|
background-color: #f7f7f7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-modal__card-left {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-modal__check {
|
2026-04-03 17:48:30 +08:00
|
|
|
|
width: 16px;
|
|
|
|
|
|
height: 16px;
|
|
|
|
|
|
border: 1px solid #d9d9d9;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
display: flex;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-04-03 17:48:30 +08:00
|
|
|
|
background: #fff;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-modal__card--active .publish-modal__check {
|
|
|
|
|
|
background-color: #1677ff;
|
|
|
|
|
|
border-color: #1677ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-modal__check-inner {
|
|
|
|
|
|
width: 8px;
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
background-color: transparent;
|
|
|
|
|
|
border: 2px solid #fff;
|
|
|
|
|
|
border-top: 0;
|
|
|
|
|
|
border-left: 0;
|
|
|
|
|
|
transform: rotate(45deg) scale(1) translate(-1px, -1px);
|
|
|
|
|
|
display: block;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 17:48:30 +08:00
|
|
|
|
.publish-modal__card-badge {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 24px;
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
background: #f7f7f7;
|
|
|
|
|
|
border: 1px solid #e5e5e5;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 700;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 17:48:30 +08:00
|
|
|
|
.publish-modal__card-text {
|
2026-04-03 00:39:15 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 17:48:30 +08:00
|
|
|
|
.publish-modal__card-name {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: #262626;
|
|
|
|
|
|
line-height: 1.3;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 17:48:30 +08:00
|
|
|
|
.publish-modal__card-sub {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
|
color: #8c8c8c;
|
|
|
|
|
|
margin-top: 2px;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 17:48:30 +08:00
|
|
|
|
.publish-modal__card-status {
|
|
|
|
|
|
font-size: 13px;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 17:48:30 +08:00
|
|
|
|
.status-selectable {
|
|
|
|
|
|
color: #52c41a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-disabled {
|
|
|
|
|
|
color: #1677ff;
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-modal__loading {
|
|
|
|
|
|
padding: 12px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 17:48:30 +08:00
|
|
|
|
.publish-modal__cover-body {
|
2026-04-13 16:08:12 +08:00
|
|
|
|
display: flex;
|
2026-04-07 09:19:03 +08:00
|
|
|
|
margin-top: 14px;
|
2026-04-03 17:48:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 16:08:12 +08:00
|
|
|
|
.publish-modal__cover-preview-wrap {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 176px;
|
|
|
|
|
|
flex: 0 0 176px;
|
2026-04-07 19:38:29 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 09:19:03 +08:00
|
|
|
|
.publish-modal__cover-preview {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-04-13 16:08:12 +08:00
|
|
|
|
width: 100%;
|
2026-04-07 09:19:03 +08:00
|
|
|
|
min-height: 124px;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
border: 1px dashed #d3dceb;
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
background:
|
|
|
|
|
|
radial-gradient(circle at top, rgba(70, 102, 255, 0.08), transparent 45%),
|
|
|
|
|
|
#fbfcff;
|
|
|
|
|
|
color: #475467;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
overflow: hidden;
|
2026-04-03 17:48:30 +08:00
|
|
|
|
}
|
2026-04-07 09:19:03 +08:00
|
|
|
|
|
|
|
|
|
|
.publish-modal__cover-preview img {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 124px;
|
|
|
|
|
|
object-fit: cover;
|
2026-04-03 17:48:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 09:19:03 +08:00
|
|
|
|
.publish-modal__cover-plus {
|
|
|
|
|
|
display: inline-flex;
|
2026-04-03 17:48:30 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-04-07 09:19:03 +08:00
|
|
|
|
width: 44px;
|
|
|
|
|
|
height: 44px;
|
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: #eef4ff;
|
|
|
|
|
|
color: #355dff;
|
|
|
|
|
|
font-size: 28px;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 16:08:12 +08:00
|
|
|
|
.publish-modal__cover-remove {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 8px;
|
|
|
|
|
|
right: 8px;
|
2026-04-07 09:19:03 +08:00
|
|
|
|
display: flex;
|
2026-04-13 16:08:12 +08:00
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 26px;
|
|
|
|
|
|
height: 26px;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.96);
|
|
|
|
|
|
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.14);
|
|
|
|
|
|
color: #ff4d4f;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: transform 0.2s ease, box-shadow 0.2s ease, color 0.2s ease;
|
2026-04-03 17:48:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-13 16:08:12 +08:00
|
|
|
|
.publish-modal__cover-remove:hover {
|
|
|
|
|
|
transform: scale(1.04);
|
|
|
|
|
|
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.18);
|
|
|
|
|
|
color: #ff7875;
|
2026-04-03 17:48:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 00:39:15 +08:00
|
|
|
|
@media (max-width: 860px) {
|
|
|
|
|
|
.publish-modal__hero {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.publish-modal__grid {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
2026-04-07 09:19:03 +08:00
|
|
|
|
|
|
|
|
|
|
.publish-modal__cover-body {
|
2026-04-13 16:08:12 +08:00
|
|
|
|
display: flex;
|
2026-04-07 09:19:03 +08:00
|
|
|
|
}
|
2026-04-03 00:39:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|