Initial commit: OmniAI Web Frontend
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
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),
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user