feat: add cover image functionality for articles
- Implemented `resolveApiURL` function to handle various URL formats. - Updated `ArticleDetail` and `UpdateArticleRequest` interfaces to include `cover_asset_url`. - Enhanced `ArticleEditorView` to manage cover image uploads, including a new `CoverPickerModal` component. - Added cover image requirements logic in `cover-requirements.ts` to enforce platform-specific cover image rules. - Modified backend services to handle cover image uploads and validations, including checks for required cover images for specific platforms. - Improved error handling for cover image requirements during article publishing.
This commit is contained in:
@@ -96,6 +96,24 @@ export function getApiBaseURL(): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
export function resolveApiURL(value?: string | null): string {
|
||||
const trimmed = String(value ?? "").trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
if (/^\/\//.test(trimmed)) {
|
||||
return `https:${trimmed}`;
|
||||
}
|
||||
const apiBaseURL = getApiBaseURL();
|
||||
if (!apiBaseURL) {
|
||||
return trimmed;
|
||||
}
|
||||
return new URL(trimmed, apiBaseURL.endsWith("/") ? apiBaseURL : `${apiBaseURL}/`).toString();
|
||||
}
|
||||
|
||||
export const apiClient = createApiClient({
|
||||
baseURL,
|
||||
auth: {
|
||||
@@ -229,7 +247,10 @@ export const articlesApi = {
|
||||
formData,
|
||||
);
|
||||
|
||||
return response.data.data;
|
||||
return {
|
||||
...response.data.data,
|
||||
url: resolveApiURL(response.data.data.url),
|
||||
};
|
||||
},
|
||||
versions(id: number) {
|
||||
return apiClient.get<ArticleVersion[]>(`/api/tenant/articles/${id}/versions`);
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { normalizePublishPlatformIds } from "./publish-platforms";
|
||||
|
||||
export type CoverSupportMode = "native" | "unsupported";
|
||||
|
||||
export interface PlatformCoverRequirement {
|
||||
platformId: string;
|
||||
required: boolean;
|
||||
supportMode: CoverSupportMode;
|
||||
aspectRatio: number | null;
|
||||
aspectLabel: string | null;
|
||||
outputWidth: number | null;
|
||||
outputHeight: number | null;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
const defaultRequirement = {
|
||||
required: false,
|
||||
supportMode: "unsupported" as CoverSupportMode,
|
||||
aspectRatio: null,
|
||||
aspectLabel: null,
|
||||
outputWidth: null,
|
||||
outputHeight: null,
|
||||
priority: 0,
|
||||
};
|
||||
|
||||
const requirementCatalog: Record<string, Partial<PlatformCoverRequirement>> = {
|
||||
baijiahao: {
|
||||
required: true,
|
||||
supportMode: "native",
|
||||
aspectRatio: 16 / 9,
|
||||
aspectLabel: "16:9",
|
||||
outputWidth: 1200,
|
||||
outputHeight: 675,
|
||||
priority: 100,
|
||||
},
|
||||
};
|
||||
|
||||
export function getPlatformCoverRequirements(platformIds?: Array<string | null | undefined>): PlatformCoverRequirement[] {
|
||||
return normalizePublishPlatformIds(platformIds).map((platformId) => {
|
||||
const config = requirementCatalog[platformId] ?? {};
|
||||
|
||||
return {
|
||||
platformId,
|
||||
required: config.required ?? defaultRequirement.required,
|
||||
supportMode: config.supportMode ?? defaultRequirement.supportMode,
|
||||
aspectRatio: config.aspectRatio ?? defaultRequirement.aspectRatio,
|
||||
aspectLabel: config.aspectLabel ?? defaultRequirement.aspectLabel,
|
||||
outputWidth: config.outputWidth ?? defaultRequirement.outputWidth,
|
||||
outputHeight: config.outputHeight ?? defaultRequirement.outputHeight,
|
||||
priority: config.priority ?? defaultRequirement.priority,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function coverUploadRequired(platformIds?: Array<string | null | undefined>): boolean {
|
||||
return getPlatformCoverRequirements(platformIds).some((item) => item.required);
|
||||
}
|
||||
|
||||
export function getPrimaryCoverRequirement(platformIds?: Array<string | null | undefined>): PlatformCoverRequirement {
|
||||
const requirements = getPlatformCoverRequirements(platformIds);
|
||||
if (!requirements.length) {
|
||||
return {
|
||||
platformId: "default",
|
||||
...defaultRequirement,
|
||||
};
|
||||
}
|
||||
|
||||
return [...requirements].sort((left, right) => right.priority - left.priority)[0];
|
||||
}
|
||||
|
||||
export function deriveCoverFileName(value?: string | null): string {
|
||||
const trimmed = String(value ?? "").trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
const pathname = new URL(trimmed, "https://cover.local").pathname;
|
||||
const lastSegment = pathname.split("/").filter(Boolean).at(-1) ?? "";
|
||||
return decodeURIComponent(lastSegment);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ const errorMessageMap: Record<string, string> = {
|
||||
object_storage_unavailable: "对象存储未配置或当前不可用",
|
||||
invalid_payload: "请求参数不合法",
|
||||
update_failed: "保存文章失败",
|
||||
publish_cover_required: "已选择百家号,请先上传封面图",
|
||||
publisher_plugin_timeout: "浏览器插件响应超时,请刷新当前页面后重试",
|
||||
publisher_plugin_empty_response: "浏览器插件未返回数据,请刷新当前页面后重试",
|
||||
publisher_plugin_invalid_response: "浏览器插件返回了无效数据,请刷新当前页面后重试",
|
||||
|
||||
Reference in New Issue
Block a user