|
@@ -24,6 +24,7 @@ export type Course = {
|
|
|
currency: "USD";
|
|
currency: "USD";
|
|
|
coverGradient: string;
|
|
coverGradient: string;
|
|
|
coverUrl?: string;
|
|
coverUrl?: string;
|
|
|
|
|
+ downloadUrl?: string;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
export type CourseVideo = {
|
|
export type CourseVideo = {
|
|
@@ -45,6 +46,23 @@ export type CourseVideoPageResult = {
|
|
|
};
|
|
};
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+export type CourseFile = {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ title: string;
|
|
|
|
|
+ introduction: string;
|
|
|
|
|
+ frontUrl?: string;
|
|
|
|
|
+ downloadUrl: string;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+export type CourseFilePageResult = {
|
|
|
|
|
+ list: CourseFile[];
|
|
|
|
|
+ page: {
|
|
|
|
|
+ current: number;
|
|
|
|
|
+ row: number;
|
|
|
|
|
+ total: number;
|
|
|
|
|
+ };
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
export const COURSE_CATEGORIES: {
|
|
export const COURSE_CATEGORIES: {
|
|
|
id: CourseCategory;
|
|
id: CourseCategory;
|
|
|
labelKey: string;
|
|
labelKey: string;
|
|
@@ -145,6 +163,7 @@ type GoodsSearchListItem = {
|
|
|
amount?: number;
|
|
amount?: number;
|
|
|
currency?: string;
|
|
currency?: string;
|
|
|
frontUrl?: string | null;
|
|
frontUrl?: string | null;
|
|
|
|
|
+ download?: string | null;
|
|
|
coverGradient?: string;
|
|
coverGradient?: string;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
@@ -158,6 +177,12 @@ type GoodsSearchListResponse = {
|
|
|
};
|
|
};
|
|
|
list?: GoodsSearchListItem[];
|
|
list?: GoodsSearchListItem[];
|
|
|
records?: GoodsSearchListItem[];
|
|
records?: GoodsSearchListItem[];
|
|
|
|
|
+ page?: {
|
|
|
|
|
+ current?: number;
|
|
|
|
|
+ row?: number;
|
|
|
|
|
+ size?: number;
|
|
|
|
|
+ total?: number;
|
|
|
|
|
+ };
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
type GoodsVideoListItem = {
|
|
type GoodsVideoListItem = {
|
|
@@ -260,6 +285,10 @@ function normalizeCourse(item: GoodsSearchListItem): Course | null {
|
|
|
goodsPriceParsed !== undefined && Number.isFinite(goodsPriceParsed)
|
|
goodsPriceParsed !== undefined && Number.isFinite(goodsPriceParsed)
|
|
|
? goodsPriceParsed
|
|
? goodsPriceParsed
|
|
|
: undefined;
|
|
: undefined;
|
|
|
|
|
+ const downloadUrl =
|
|
|
|
|
+ typeof item.download === "string" && item.download.trim()
|
|
|
|
|
+ ? item.download.trim()
|
|
|
|
|
+ : undefined;
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
id,
|
|
id,
|
|
@@ -274,6 +303,7 @@ function normalizeCourse(item: GoodsSearchListItem): Course | null {
|
|
|
price: Number.isFinite(price) ? price : 0,
|
|
price: Number.isFinite(price) ? price : 0,
|
|
|
currency,
|
|
currency,
|
|
|
coverUrl: typeof item.frontUrl === "string" ? item.frontUrl : undefined,
|
|
coverUrl: typeof item.frontUrl === "string" ? item.frontUrl : undefined,
|
|
|
|
|
+ downloadUrl,
|
|
|
coverGradient: item.coverGradient ?? "from-slate-700 via-slate-800 to-slate-900",
|
|
coverGradient: item.coverGradient ?? "from-slate-700 via-slate-800 to-slate-900",
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
@@ -363,6 +393,87 @@ function extractVideoPage(payload: GoodsVideoListResponse): {
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function extractSearchPage(payload: GoodsSearchListResponse): {
|
|
|
|
|
+ current: number;
|
|
|
|
|
+ row: number;
|
|
|
|
|
+ total: number;
|
|
|
|
|
+} {
|
|
|
|
|
+ const current = Number(payload.page?.current);
|
|
|
|
|
+ const row = Number(payload.page?.row ?? payload.page?.size);
|
|
|
|
|
+ const total = Number(payload.page?.total);
|
|
|
|
|
+ return {
|
|
|
|
|
+ current: Number.isFinite(current) && current > 0 ? current : 1,
|
|
|
|
|
+ row: Number.isFinite(row) && row > 0 ? row : 10,
|
|
|
|
|
+ total: Number.isFinite(total) && total >= 0 ? total : 0,
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function parseDownloadUrls(rawDownload: unknown): string[] {
|
|
|
|
|
+ if (typeof rawDownload !== "string") return [];
|
|
|
|
|
+ const input = rawDownload.trim();
|
|
|
|
|
+ if (!input) return [];
|
|
|
|
|
+
|
|
|
|
|
+ const normalize = (value: string): string =>
|
|
|
|
|
+ value
|
|
|
|
|
+ .trim()
|
|
|
|
|
+ .replace(/^['"]+|['"]+$/g, "")
|
|
|
|
|
+ .trim();
|
|
|
|
|
+
|
|
|
|
|
+ const parseFromJson = (source: string): string[] | null => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const parsed: unknown = JSON.parse(source);
|
|
|
|
|
+ if (Array.isArray(parsed)) {
|
|
|
|
|
+ return parsed
|
|
|
|
|
+ .filter((entry): entry is string => typeof entry === "string")
|
|
|
|
|
+ .map(normalize)
|
|
|
|
|
+ .filter(Boolean);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (typeof parsed === "string") {
|
|
|
|
|
+ return [normalize(parsed)].filter(Boolean);
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const direct = parseFromJson(input);
|
|
|
|
|
+ if (direct && direct.length > 0) return direct;
|
|
|
|
|
+
|
|
|
|
|
+ const unescaped = input.replace(/\\"/g, '"');
|
|
|
|
|
+ const secondTry = parseFromJson(unescaped);
|
|
|
|
|
+ if (secondTry && secondTry.length > 0) return secondTry;
|
|
|
|
|
+
|
|
|
|
|
+ const trimmedBrackets = unescaped.replace(/^\[/, "").replace(/\]$/, "");
|
|
|
|
|
+ return trimmedBrackets
|
|
|
|
|
+ .split(",")
|
|
|
|
|
+ .map(normalize)
|
|
|
|
|
+ .filter(Boolean);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function normalizeFile(item: GoodsSearchListItem): CourseFile[] {
|
|
|
|
|
+ const id = String(item.goodsId ?? item.id ?? "").trim();
|
|
|
|
|
+ if (!id) return [];
|
|
|
|
|
+ const title = String(item.title ?? item.goodsName ?? item.name ?? "").trim();
|
|
|
|
|
+ if (!title) return [];
|
|
|
|
|
+ const downloadUrls = parseDownloadUrls(item.download);
|
|
|
|
|
+ if (downloadUrls.length === 0) return [];
|
|
|
|
|
+ const introduction = String(
|
|
|
|
|
+ item.introduction ?? item.subtitle ?? item.subTitle ?? item.intro ?? "",
|
|
|
|
|
+ ).trim();
|
|
|
|
|
+ const frontUrl =
|
|
|
|
|
+ typeof item.frontUrl === "string" && item.frontUrl.trim()
|
|
|
|
|
+ ? item.frontUrl.trim()
|
|
|
|
|
+ : undefined;
|
|
|
|
|
+ return downloadUrls.map((downloadUrl, index) => ({
|
|
|
|
|
+ id: `${id}-${index + 1}`,
|
|
|
|
|
+ title,
|
|
|
|
|
+ introduction,
|
|
|
|
|
+ frontUrl,
|
|
|
|
|
+ downloadUrl,
|
|
|
|
|
+ }));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
export async function fetchCourses(category?: CourseCategory): Promise<Course[]> {
|
|
export async function fetchCourses(category?: CourseCategory): Promise<Course[]> {
|
|
|
try {
|
|
try {
|
|
|
const payload = await apiPost<
|
|
const payload = await apiPost<
|
|
@@ -424,3 +535,37 @@ export async function fetchCourseVideos(
|
|
|
return { list: [], page: { current, row, total: 0 } };
|
|
return { list: [], page: { current, row, total: 0 } };
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+export async function fetchCourseFiles(
|
|
|
|
|
+ goodsId: string,
|
|
|
|
|
+ page: { current: number; row: number } = { current: 1, row: 10 },
|
|
|
|
|
+): Promise<CourseFilePageResult> {
|
|
|
|
|
+ const normalizedGoodsId = goodsId.trim();
|
|
|
|
|
+ const current = Number.isFinite(page.current) ? Math.max(1, page.current) : 1;
|
|
|
|
|
+ const row = Number.isFinite(page.row) ? Math.max(1, page.row) : 10;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const payload = await apiPost<
|
|
|
|
|
+ GoodsSearchListResponse,
|
|
|
|
|
+ { goodsType: GoodsType; page: { current: number; row: number } }
|
|
|
|
|
+ >("/goods/search/list", {
|
|
|
|
|
+ goodsType: 5,
|
|
|
|
|
+ page: { current, row },
|
|
|
|
|
+ });
|
|
|
|
|
+ const list = extractList(payload).flatMap(normalizeFile);
|
|
|
|
|
+ const matched = normalizedGoodsId
|
|
|
|
|
+ ? list.filter((f) => f.id.startsWith(`${normalizedGoodsId}-`))
|
|
|
|
|
+ : [];
|
|
|
|
|
+ const finalList = matched.length > 0 ? matched : list;
|
|
|
|
|
+ const serverPage = extractSearchPage(payload);
|
|
|
|
|
+ return {
|
|
|
|
|
+ list: finalList,
|
|
|
|
|
+ page: {
|
|
|
|
|
+ current: serverPage.current || current,
|
|
|
|
|
+ row: serverPage.row || row,
|
|
|
|
|
+ total: matched.length > 0 ? matched.length : serverPage.total || finalList.length,
|
|
|
|
|
+ },
|
|
|
|
|
+ };
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return { list: [], page: { current, row, total: 0 } };
|
|
|
|
|
+ }
|
|
|
|
|
+}
|