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; 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; 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; assets?: Array<{ assetType?: ServerCommunityAsset["assetType"]; title?: string; url?: string; ossKey?: string; metadata?: Record; 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 { 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 { 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(`community/cases?${search.toString()}`)); }, async listMyCases(): Promise { return extractCases(await serverRequest("community/me/cases")); }, async listCasesForReview(status: "" | ServerCommunityCase["status"] = "pending"): Promise { const search = new URLSearchParams(); if (status) search.set("status", status); const query = search.toString(); return extractCases(await serverRequest(`admin/community/cases${query ? `?${query}` : ""}`)); }, async publishCase(input: PublishCommunityCaseInput): Promise { const payload = await serverRequest<{ case: unknown }>("community/cases", { method: "POST", body: input, }); return normalizeCase(payload.case); }, async updateReviewStatus( caseId: number | string, status: Exclude, reviewNote: string, ): Promise { 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 { 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), }; }, };