feat: 分类接口替换为api 接口
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { getApiConfig } from '../config/api';
|
||||
|
||||
interface ApiEnvelope<T> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
/** 保留有限的状态用于页面反馈,不把服务器原始响应或凭证输出到界面。 */
|
||||
export class ApiRequestError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly statusCode: number = 0,
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ApiRequestError';
|
||||
}
|
||||
}
|
||||
|
||||
/** 新后端统一请求层:校验 HTTP 与业务状态,旧云函数/CDN 请求保持独立。 */
|
||||
export async function apiRequest<T>(
|
||||
path: string,
|
||||
options: {
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
data?: Record<string, unknown>;
|
||||
header?: Record<string, string>;
|
||||
} = {},
|
||||
): Promise<T> {
|
||||
const config = getApiConfig();
|
||||
if (!path.startsWith('/') || path.startsWith('//'))
|
||||
throw new Error('接口路径不正确');
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request<ApiEnvelope<T>>({
|
||||
url: `${config.baseUrl}${path}`,
|
||||
method: options.method || 'GET',
|
||||
data: options.data,
|
||||
header: { 'Content-Type': 'application/json', ...options.header },
|
||||
timeout: config.timeout,
|
||||
success(res) {
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
reject(
|
||||
new ApiRequestError(
|
||||
res.statusCode === 404
|
||||
? '内容暂不可用'
|
||||
: '接口请求失败,请稍后重试',
|
||||
res.statusCode,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!res.data ||
|
||||
res.data.code !== 0 ||
|
||||
!Object.prototype.hasOwnProperty.call(res.data, 'data')
|
||||
) {
|
||||
reject(
|
||||
new ApiRequestError(
|
||||
'接口返回异常,请稍后重试',
|
||||
res.statusCode,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
resolve(res.data.data);
|
||||
},
|
||||
fail() {
|
||||
reject(
|
||||
new ApiRequestError('连接失败,请确认后端已启动及网络配置'),
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { apiRequest } from './api-request';
|
||||
|
||||
/** 小程序使用的公开接口字段,不复用旧云集合的 _id 和管理配置。 */
|
||||
export interface ApiCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
parentId: string | null;
|
||||
status: 'active';
|
||||
}
|
||||
|
||||
export interface ApiWorksheet {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
category: string;
|
||||
path: string;
|
||||
ageMin: number;
|
||||
ageMax: number;
|
||||
difficulty: 1 | 2 | 3 | 4;
|
||||
previewImg: string | null;
|
||||
likes: string;
|
||||
downloads: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
contentUpdatedAt: string | null;
|
||||
}
|
||||
|
||||
interface WorksheetPage {
|
||||
items: ApiWorksheet[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
hasNext: boolean;
|
||||
}
|
||||
|
||||
/** 分类直接由服务端发布状态决定,不用本地目录补齐或恢复下架分类。 */
|
||||
export async function listCategories(): Promise<ApiCategory[]> {
|
||||
const rows = await apiRequest<ApiCategory[]>('/categories');
|
||||
if (
|
||||
!Array.isArray(rows) ||
|
||||
rows.some(
|
||||
(row) =>
|
||||
!row ||
|
||||
typeof row.id !== 'string' ||
|
||||
!row.id ||
|
||||
typeof row.name !== 'string' ||
|
||||
typeof row.icon !== 'string' ||
|
||||
row.status !== 'active',
|
||||
)
|
||||
) {
|
||||
throw new Error('分类数据异常,请稍后重试');
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** 展示计数保持十进制字符串,不转换 Number 或重复叠加展示种子。 */
|
||||
function validateWorksheet(row: ApiWorksheet): void {
|
||||
if (
|
||||
!row ||
|
||||
!row.id ||
|
||||
typeof row.id !== 'string' ||
|
||||
typeof row.title !== 'string' ||
|
||||
typeof row.subtitle !== 'string' ||
|
||||
typeof row.category !== 'string' ||
|
||||
typeof row.path !== 'string' ||
|
||||
!Number.isInteger(row.ageMin) ||
|
||||
!Number.isInteger(row.ageMax) ||
|
||||
row.ageMin < 0 ||
|
||||
row.ageMax < row.ageMin ||
|
||||
![1, 2, 3, 4].includes(row.difficulty) ||
|
||||
typeof row.downloads !== 'string' ||
|
||||
!/^\d+$/.test(row.downloads) ||
|
||||
typeof row.likes !== 'string' ||
|
||||
!/^\d+$/.test(row.likes) ||
|
||||
(row.previewImg !== null && typeof row.previewImg !== 'string')
|
||||
) {
|
||||
throw new Error('题型数据异常,请稍后重试');
|
||||
}
|
||||
}
|
||||
|
||||
/** 取完整分页后才能在客户端计算收藏榜和混合排序;分页变化或缺页时整次重试。 */
|
||||
export async function listAllWorksheets(): Promise<ApiWorksheet[]> {
|
||||
const pageSize = 100;
|
||||
const rows: ApiWorksheet[] = [];
|
||||
const ids = new Set<string>();
|
||||
let expectedTotal: number | undefined;
|
||||
for (let page = 1; ; page++) {
|
||||
const result = await apiRequest<WorksheetPage>('/worksheets', {
|
||||
data: { page, pageSize },
|
||||
});
|
||||
if (
|
||||
!result ||
|
||||
!Array.isArray(result.items) ||
|
||||
!Number.isSafeInteger(result.total) ||
|
||||
result.total < 0 ||
|
||||
result.page !== page ||
|
||||
result.pageSize !== pageSize ||
|
||||
result.hasNext !== page * pageSize < result.total ||
|
||||
result.items.length !==
|
||||
Math.max(
|
||||
0,
|
||||
Math.min(pageSize, result.total - (page - 1) * pageSize),
|
||||
) ||
|
||||
(expectedTotal !== undefined && result.total !== expectedTotal)
|
||||
) {
|
||||
throw new Error('题型列表已变化,请重试');
|
||||
}
|
||||
expectedTotal = result.total;
|
||||
for (const row of result.items) {
|
||||
validateWorksheet(row);
|
||||
if (ids.has(row.id)) throw new Error('题型列表已变化,请重试');
|
||||
ids.add(row.id);
|
||||
rows.push(row);
|
||||
}
|
||||
if (!result.hasNext) return rows;
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开题型前重新检查公开详情,避免已下架内容从旧列表继续进入。 */
|
||||
export async function getWorksheet(id: string): Promise<ApiWorksheet> {
|
||||
const row = await apiRequest<ApiWorksheet>(
|
||||
`/worksheets/${encodeURIComponent(id)}`,
|
||||
);
|
||||
validateWorksheet(row);
|
||||
if (row.id !== id) throw new Error('题型数据异常,请稍后重试');
|
||||
return row;
|
||||
}
|
||||
Reference in New Issue
Block a user