Files
omniai-web/src/api/communityClient.ts
T
2026-06-02 12:38:01 +08:00

204 lines
7.3 KiB
TypeScript

import { isRecord, serverRequest } from "./serverConnection";
export interface ServerCommunityAsset {
id?: number;
assetType: "image" | "video" | "project" | "workflow" | "asset" | "cover" | "other";
title?: string | null;
url?: string | null;
ossKey?: string | null;
metadata?: Record<string, unknown>;
sortOrder?: number;
}
export interface ServerCommunityCase {
id: number;
userId?: number;
username?: string | null;
projectId?: string | null;
title: string;
description?: string | null;
coverUrl?: string | null;
tags: string[];
metadata: Record<string, unknown>;
status: "pending" | "approved" | "rejected";
reviewNote?: string | null;
publishedAt?: string | null;
copyCount: number;
favoriteCount: number;
likeCount: number;
isFavorited: boolean;
isLiked: boolean;
createdAt: string;
updatedAt: string;
assets: ServerCommunityAsset[];
}
export interface PublishCommunityCaseInput {
projectId?: string | null;
title: string;
description?: string | null;
coverUrl?: string | null;
tags?: string[];
metadata?: Record<string, unknown>;
assets?: Array<{
assetType?: ServerCommunityAsset["assetType"];
title?: string;
url?: string;
ossKey?: string;
metadata?: Record<string, unknown>;
sortOrder?: number;
}>;
}
function toNumber(value: unknown, fallback = 0): number {
const numeric = typeof value === "number" ? value : Number(value);
return Number.isFinite(numeric) ? numeric : fallback;
}
function toStringValue(value: unknown, fallback = ""): string {
if (typeof value === "string") return value.trim() || fallback;
if (typeof value === "number" && Number.isFinite(value)) return String(value);
return fallback;
}
function toStringArray(value: unknown): string[] {
return Array.isArray(value)
? value.map((item) => toStringValue(item)).filter(Boolean)
: [];
}
function toMetadata(value: unknown): Record<string, unknown> {
return isRecord(value) ? value : {};
}
function normalizeAsset(raw: unknown): ServerCommunityAsset {
const asset = isRecord(raw) ? raw : {};
const assetType = toStringValue(asset.assetType ?? asset.asset_type ?? asset.type, "other");
return {
id: Number.isFinite(Number(asset.id)) ? Number(asset.id) : undefined,
assetType:
assetType === "image" ||
assetType === "video" ||
assetType === "image" ||
assetType === "project" ||
assetType === "workflow" ||
assetType === "asset" ||
assetType === "cover"
? assetType
: "other",
title: toStringValue(asset.title) || null,
url: toStringValue(asset.url) || null,
ossKey: toStringValue(asset.ossKey ?? asset.oss_key) || null,
metadata: toMetadata(asset.metadata),
sortOrder: toNumber(asset.sortOrder ?? asset.sort_order),
};
}
function normalizeCase(raw: unknown): ServerCommunityCase {
const item = isRecord(raw) ? raw : {};
const status = toStringValue(item.status, "pending");
return {
id: toNumber(item.id),
userId: Number.isFinite(Number(item.userId ?? item.user_id)) ? Number(item.userId ?? item.user_id) : undefined,
username: toStringValue(item.username) || null,
projectId: toStringValue(item.projectId ?? item.project_id) || null,
title: toStringValue(item.title, "未命名案例"),
description: toStringValue(item.description) || null,
coverUrl: toStringValue(item.coverUrl ?? item.cover_url) || null,
tags: toStringArray(item.tags),
metadata: toMetadata(item.metadata),
status: status === "approved" || status === "rejected" ? status : "pending",
reviewNote: toStringValue(item.reviewNote ?? item.review_note) || null,
publishedAt: toStringValue(item.publishedAt ?? item.published_at) || null,
copyCount: toNumber(item.copyCount ?? item.copy_count),
favoriteCount: toNumber(item.favoriteCount ?? item.favorite_count),
likeCount: toNumber(item.likeCount ?? item.like_count),
isFavorited: Boolean(item.isFavorited ?? item.is_favorited),
isLiked: Boolean(item.isLiked ?? item.is_liked),
createdAt: toStringValue(item.createdAt ?? item.created_at, new Date().toISOString()),
updatedAt: toStringValue(item.updatedAt ?? item.updated_at, new Date().toISOString()),
assets: Array.isArray(item.assets) ? item.assets.map(normalizeAsset) : [],
};
}
function extractCases(payload: unknown): ServerCommunityCase[] {
if (Array.isArray(payload)) return payload.map(normalizeCase);
if (!isRecord(payload)) return [];
const rows = payload.cases ?? payload.items;
return Array.isArray(rows) ? rows.map(normalizeCase) : [];
}
export const communityClient = {
async listApprovedCases(
params: number | { limit?: number; q?: string; category?: string; tag?: string; sort?: string } = 30,
): Promise<ServerCommunityCase[]> {
const search = new URLSearchParams();
if (typeof params === "number") {
search.set("limit", String(params));
} else {
search.set("limit", String(params.limit ?? 30));
if (params.q) search.set("q", params.q);
if (params.category && params.category !== "全部") search.set("category", params.category);
if (params.tag) search.set("tag", params.tag);
if (params.sort) search.set("sort", params.sort);
}
return extractCases(await serverRequest<unknown>(`community/cases?${search.toString()}`));
},
async listMyCases(): Promise<ServerCommunityCase[]> {
return extractCases(await serverRequest<unknown>("community/me/cases"));
},
async listCasesForReview(status: "" | ServerCommunityCase["status"] = "pending"): Promise<ServerCommunityCase[]> {
const search = new URLSearchParams();
if (status) search.set("status", status);
const query = search.toString();
return extractCases(await serverRequest<unknown>(`admin/community/cases${query ? `?${query}` : ""}`));
},
async publishCase(input: PublishCommunityCaseInput): Promise<ServerCommunityCase> {
const payload = await serverRequest<{ case: unknown }>("community/cases", {
method: "POST",
body: input,
});
return normalizeCase(payload.case);
},
async updateReviewStatus(
caseId: number | string,
status: Exclude<ServerCommunityCase["status"], "pending">,
reviewNote: string,
): Promise<ServerCommunityCase> {
const payload = await serverRequest<{ case: unknown }>(`admin/community/cases/${encodeURIComponent(String(caseId))}/status`, {
method: "PATCH",
body: { status, reviewNote, review_note: reviewNote },
});
return normalizeCase(payload.case);
},
async copyCase(caseId: number, input?: { projectId?: string; name?: string; ossKey?: string }): Promise<void> {
await serverRequest(`community/cases/${caseId}/copy`, {
method: "POST",
body: input || {},
});
},
async setReaction(
caseId: number,
reactionType: "favorite" | "like",
active: boolean,
): Promise<{ favoriteCount: number; likeCount: number; isFavorited: boolean; isLiked: boolean }> {
const payload = await serverRequest<{ stats: unknown }>(`community/cases/${caseId}/reactions`, {
method: "POST",
body: { reactionType, active },
});
const stats = isRecord(payload.stats) ? payload.stats : {};
return {
favoriteCount: toNumber(stats.favoriteCount),
likeCount: toNumber(stats.likeCount),
isFavorited: Boolean(stats.isFavorited),
isLiked: Boolean(stats.isLiked),
};
},
};