feat(admin-web): trim platform and cover controls from article editor

Remove the right-rail publish platform selector and cover picker from the
in-editor view; those settings now live in the publish modal so the editor
focuses on title and content.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 23:16:45 +08:00
parent e38c58e3d7
commit cde70ca5e0
+4 -278
View File
@@ -7,19 +7,9 @@ import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
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, tenantAccountsApi } from "@/lib/api";
import {
coverUploadRequired,
deriveCoverFileName,
} from "@/lib/cover-requirements";
import { articlesApi } from "@/lib/api";
import { formatError } from "@/lib/errors";
import {
buildPublishPlatformOptions,
normalizePublishPlatformIds,
} from "@/lib/publish-platforms";
const route = useRoute();
const router = useRouter();
@@ -31,15 +21,6 @@ const title = ref("");
const markdown = ref("");
const initialTitle = ref("");
const initialMarkdown = ref("");
const publishPlatforms = ref<string[]>([]);
const initialPublishPlatforms = ref<string[]>([]);
const coverEnabled = ref(false);
const coverAssetUrl = ref("");
const coverImageAssetId = ref<number | null>(null);
const coverFileName = ref("");
const initialCoverAssetUrl = ref("");
const initialCoverImageAssetId = ref<number | null>(null);
const coverPickerOpen = ref(false);
const leaveModalOpen = ref(false);
const publishModalOpen = ref(false);
@@ -49,24 +30,6 @@ const detailQuery = useQuery({
queryFn: () => articlesApi.detail(articleId.value),
});
const platformsQuery = useQuery({
queryKey: ["media", "platforms", "article-editor"],
queryFn: () => mediaApi.platforms(),
});
const accountsQuery = useQuery({
queryKey: ["tenant", "desktop-accounts", "article-editor"],
queryFn: () => tenantAccountsApi.list(),
});
const publishPlatformOptions = computed(() =>
buildPublishPlatformOptions(platformsQuery.data.value ?? [], accountsQuery.data.value),
);
const publishPlatformsLoading = computed(
() => platformsQuery.isPending.value || accountsQuery.isPending.value,
);
watch(
() => detailQuery.data.value,
(detail) => {
@@ -81,45 +44,17 @@ watch(
markdown.value = nextMarkdown;
initialTitle.value = nextTitle;
initialMarkdown.value = nextMarkdown;
publishPlatforms.value = normalizePublishPlatformIds(detail.platforms ?? []);
initialPublishPlatforms.value = [...publishPlatforms.value];
coverAssetUrl.value = resolveApiURL(detail.cover_asset_url);
coverImageAssetId.value = detail.cover_image_asset_id ?? null;
coverFileName.value = deriveCoverFileName(coverAssetUrl.value);
initialCoverAssetUrl.value = coverAssetUrl.value;
initialCoverImageAssetId.value = coverImageAssetId.value;
coverEnabled.value = Boolean(coverAssetUrl.value);
},
{ immediate: true },
);
const detail = computed(() => detailQuery.data.value);
const editorLocked = computed(() => detail.value?.generate_status !== "completed");
const coverRequired = computed(() => coverUploadRequired(publishPlatforms.value));
const normalizedCoverValue = computed(() => {
if (!coverEnabled.value && !coverRequired.value) {
return "";
}
return coverAssetUrl.value.trim();
});
const referencedImageAssetIds = computed(() => extractReferencedImageAssetIds(markdown.value));
const hasChanges = computed(
() =>
title.value.trim() !== initialTitle.value.trim() ||
markdown.value !== initialMarkdown.value ||
serializePlatformSelection(publishPlatforms.value) !== serializePlatformSelection(initialPublishPlatforms.value) ||
normalizedCoverValue.value !== initialCoverAssetUrl.value.trim() ||
coverImageAssetId.value !== initialCoverImageAssetId.value,
);
watch(
coverRequired,
(required) => {
if (required) {
coverEnabled.value = true;
}
},
{ immediate: true },
markdown.value !== initialMarkdown.value,
);
const saveMutation = useMutation({
@@ -127,10 +62,7 @@ const saveMutation = useMutation({
articlesApi.update(articleId.value, {
title: title.value.trim(),
markdown_content: markdown.value,
platforms: normalizePublishPlatformIds(publishPlatforms.value),
cover_asset_url: normalizedCoverValue.value,
referenced_image_asset_ids: referencedImageAssetIds.value,
cover_image_asset_id: coverImageAssetId.value,
}),
onSuccess: async (data) => {
message.success(t("article.editor.messages.saved"));
@@ -138,14 +70,6 @@ const saveMutation = useMutation({
markdown.value = data.markdown_content ?? "";
initialTitle.value = title.value;
initialMarkdown.value = markdown.value;
publishPlatforms.value = normalizePublishPlatformIds(data.platforms ?? []);
initialPublishPlatforms.value = [...publishPlatforms.value];
coverAssetUrl.value = resolveApiURL(data.cover_asset_url);
coverImageAssetId.value = data.cover_image_asset_id ?? null;
coverFileName.value = deriveCoverFileName(coverAssetUrl.value);
initialCoverAssetUrl.value = coverAssetUrl.value;
initialCoverImageAssetId.value = coverImageAssetId.value;
coverEnabled.value = coverRequired.value || Boolean(coverAssetUrl.value);
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["articles"] }),
queryClient.invalidateQueries({ queryKey: ["workspace"] }),
@@ -222,19 +146,6 @@ async function uploadEditorImage(file: File): Promise<string> {
return result.url;
}
function handleCoverPicked(payload: { url: string; fileName: string; assetId?: number | null }): void {
coverAssetUrl.value = payload.url;
coverImageAssetId.value = payload.assetId ?? null;
coverFileName.value = payload.fileName;
coverEnabled.value = true;
}
function handleRemoveCover(): void {
coverAssetUrl.value = "";
coverImageAssetId.value = null;
coverFileName.value = "";
}
async function handlePublished(): Promise<void> {
await Promise.all([
detailQuery.refetch(),
@@ -353,12 +264,6 @@ function extractReferencedImageAssetIds(markdownValue: string): number[] {
return [...ids];
}
function serializePlatformSelection(platformIds: string[]): string {
return normalizePublishPlatformIds(platformIds)
.sort()
.join(",");
}
onMounted(() => {
window.addEventListener("keydown", handleSaveShortcut);
});
@@ -415,63 +320,6 @@ onBeforeUnmount(() => {
:upload-image="uploadEditorImage"
/>
</section>
<aside class="article-editor-view__rail">
<section class="article-editor-view__card">
<h3>{{ t("article.editor.platformsTitle") }}</h3>
<p>{{ t("article.editor.platformsHint") }}</p>
<a-skeleton
v-if="publishPlatformsLoading"
active
:title="false"
:paragraph="{ rows: 4 }"
/>
<PublishPlatformSelector
v-else
v-model="publishPlatforms"
class="article-editor-view__platforms"
:platforms="publishPlatformOptions"
:disabled="editorLocked"
/>
</section>
<section class="article-editor-view__card">
<div class="article-editor-view__card-head">
<div>
<h3>{{ t("article.editor.coverTitle") }}</h3>
<p>{{ coverRequired ? t("article.editor.coverRequired") : t("article.editor.coverHint") }}</p>
</div>
<a-switch
v-model:checked="coverEnabled"
:disabled="coverRequired"
/>
</div>
<div
v-if="coverEnabled || coverRequired"
class="article-editor-view__cover-panel"
>
<button
type="button"
class="article-editor-view__cover-dropzone"
@click="coverPickerOpen = true"
>
<template v-if="coverAssetUrl">
<img :src="coverAssetUrl" alt="cover preview" />
</template>
<template v-else>
<span class="article-editor-view__cover-plus">+</span>
<span>{{ t("article.editor.coverUpload") }}</span>
</template>
</button>
</div>
<div v-else class="article-editor-view__cover-off">
{{ t("article.editor.coverOff") }}
</div>
</section>
</aside>
</div>
</template>
@@ -511,16 +359,6 @@ onBeforeUnmount(() => {
:article-id="detail?.id ?? null"
@published="handlePublished"
/>
<CoverPickerModal
v-model:open="coverPickerOpen"
:article-id="detail?.id ?? null"
:platform-ids="publishPlatforms"
:current-url="coverAssetUrl"
:current-file-name="coverFileName"
:current-asset-id="coverImageAssetId"
@confirmed="handleCoverPicked"
/>
</div>
</template>
@@ -577,8 +415,7 @@ onBeforeUnmount(() => {
backdrop-filter: blur(14px);
}
.article-editor-view__loading,
.article-editor-view__card {
.article-editor-view__loading {
padding: 24px;
background: rgba(255, 255, 255, 0.88);
border: 1px solid rgba(216, 225, 237, 0.9);
@@ -598,10 +435,7 @@ onBeforeUnmount(() => {
}
.article-editor-view__layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 360px;
gap: 18px;
align-items: start;
min-width: 0;
}
.article-editor-view__main {
@@ -610,114 +444,6 @@ onBeforeUnmount(() => {
gap: 18px;
}
.article-editor-view__card h3 {
margin: 0;
color: #101828;
font-size: 15px;
font-weight: 700;
}
.article-editor-view__rail {
display: flex;
flex-direction: column;
gap: 18px;
}
.article-editor-view__card p {
margin: 6px 0 0;
color: #667085;
font-size: 13px;
line-height: 1.7;
}
.article-editor-view__platforms {
margin-top: 18px;
}
.article-editor-view__card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.article-editor-view__cover-panel {
display: grid;
grid-template-columns: 156px minmax(0, 1fr);
gap: 16px;
margin-top: 18px;
}
.article-editor-view__cover-dropzone {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 116px;
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;
}
.article-editor-view__cover-dropzone img {
width: 100%;
height: 116px;
object-fit: cover;
}
.article-editor-view__cover-info {
display: flex;
flex-direction: column;
gap: 12px;
}
.article-editor-view__cover-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.article-editor-view__cover-file,
.article-editor-view__cover-off {
color: #667085;
font-size: 13px;
line-height: 1.7;
}
.article-editor-view__cover-plus {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 999px;
background: #eef4ff;
color: #355dff;
font-size: 28px;
line-height: 1;
}
@media (max-width: 1200px) {
.article-editor-view__layout {
grid-template-columns: 1fr;
}
.article-editor-view__rail {
order: -1;
}
.article-editor-view__cover-panel {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.article-editor-view__topbar {
flex-direction: column;