Files
digital-pilates/services/challengesApi.ts
richarjiang b0602b0a99 feat(challenges): 新增挑战详情页与排行榜及轮播卡片交互
- 重构挑战列表为横向轮播,支持多进行中的挑战
- 新增挑战详情页 /challenges/[id]/index 与排行榜 /challenges/[id]/leaderboard
- ChallengeProgressCard 支持小时级剩余时间显示
- 新增 ChallengeRankingItem 组件展示榜单项
- 排行榜支持分页加载、下拉刷新与错误重试
- 挑战卡片新增已结束角标与渐变遮罩
- 加入/退出挑战时展示庆祝动画与错误提示
- 统一背景渐变色与卡片阴影细节
2025-09-30 11:33:24 +08:00

103 lines
2.7 KiB
TypeScript

import { api } from './api';
export type ChallengeStatus = 'upcoming' | 'ongoing' | 'expired';
export type ChallengeProgressDto = {
completed: number;
target: number;
remaining: number
checkedInToday: boolean;
};
export type RankingItemDto = {
id: string;
name: string;
avatar: string | null;
metric: string;
badge?: string;
todayReportedValue?: number;
todayTargetValue?: number;
};
export enum ChallengeType {
WATER = 'water',
EXERCISE = 'exercise',
DIET = 'diet',
MOOD = 'mood',
SLEEP = 'sleep',
WEIGHT = 'weight',
}
export type ChallengeListItemDto = {
id: string;
title: string;
image: string;
periodLabel?: string;
durationLabel: string;
requirementLabel: string;
status: ChallengeStatus;
participantsCount: number;
rankingDescription?: string;
highlightTitle: string;
highlightSubtitle: string;
ctaLabel: string;
progress?: ChallengeProgressDto;
isJoined: boolean;
startAt?: string;
endAt?: string;
minimumCheckInDays: number; // 最小打卡天数
type: ChallengeType;
};
export type ChallengeDetailDto = ChallengeListItemDto & {
summary?: string;
rankings: RankingItemDto[];
userRank?: number;
};
export type ChallengeRankingsDto = {
total: number;
page: number;
pageSize: number;
items: RankingItemDto[];
};
export async function listChallenges(): Promise<ChallengeListItemDto[]> {
return api.get<ChallengeListItemDto[]>('/challenges');
}
export async function getChallengeDetail(id: string): Promise<ChallengeDetailDto> {
return api.get<ChallengeDetailDto>(`/challenges/${encodeURIComponent(id)}`);
}
export async function joinChallenge(id: string): Promise<ChallengeProgressDto> {
return api.post<ChallengeProgressDto>(`/challenges/${encodeURIComponent(id)}/join`);
}
export async function leaveChallenge(id: string): Promise<boolean> {
return api.post<boolean>(`/challenges/${encodeURIComponent(id)}/leave`);
}
export async function reportChallengeProgress(id: string, value?: number): Promise<ChallengeProgressDto> {
const body = value != null ? { value } : undefined;
return api.post<ChallengeProgressDto>(`/challenges/${encodeURIComponent(id)}/progress`, body);
}
export async function getChallengeRankings(
id: string,
params?: { page?: number; pageSize?: number }
): Promise<ChallengeRankingsDto> {
const searchParams = new URLSearchParams();
if (params?.page) {
searchParams.append('page', String(params.page));
}
if (params?.pageSize) {
searchParams.append('pageSize', String(params.pageSize));
}
const query = searchParams.toString();
const url = `/challenges/${encodeURIComponent(id)}/rankings${query ? `?${query}` : ''}`;
return api.get<ChallengeRankingsDto>(url);
}