Files
digital-pilates/services/challengesApi.ts
richarjiang 39671ed70f feat(challenges): 添加自定义挑战功能和多语言支持
- 新增自定义挑战创建页面,支持设置挑战类型、时间范围、目标值等
- 实现挑战邀请码系统,支持通过邀请码加入自定义挑战
- 完善挑战详情页面的多语言翻译支持
- 优化用户认证状态检查逻辑,使用token作为主要判断依据
- 添加阿里字体文件支持,提升UI显示效果
- 改进确认弹窗组件,支持Liquid Glass效果和自定义内容
- 优化应用启动流程,直接读取onboarding状态而非预加载用户数据
2025-11-26 16:39:01 +08:00

193 lines
5.1 KiB
TypeScript

import { api } from './api';
export type ChallengeStatus = 'upcoming' | 'ongoing' | 'expired';
export enum ChallengeSource {
SYSTEM = 'system',
CUSTOM = 'custom',
}
export enum ChallengeState {
DRAFT = 'draft',
ACTIVE = 'active',
ARCHIVED = 'archived',
}
export type ChallengeProgressDto = {
completed: number;
target: number;
remaining: number
checkedInToday: boolean;
lastProgressAt?: string;
last_progress_at?: string;
};
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;
unit?: 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;
shareCode?: string | null;
source?: ChallengeSource;
creatorId?: string | null;
isCreator?: boolean;
isPublic?: boolean;
maxParticipants?: number | null;
challengeState?: ChallengeState;
progressUnit?: string;
targetValue?: number;
summary?: string | null;
};
export type ChallengeDetailDto = ChallengeListItemDto & {
summary?: string | null;
rankings?: RankingItemDto[];
userRank?: number;
challengeState?: ChallengeState;
};
export type ChallengeRankingsDto = {
total: number;
page: number;
pageSize: number;
items: RankingItemDto[];
};
export type ChallengeListResponse = {
items: ChallengeListItemDto[];
total: number;
page: number;
pageSize: number;
};
export type CreateCustomChallengePayload = {
title: string;
type: ChallengeType;
image?: string | null;
startAt: number;
endAt: number;
targetValue: number;
minimumCheckInDays: number;
durationLabel: string;
requirementLabel: string;
summary?: string | null;
progressUnit?: string;
periodLabel?: string | null;
rankingDescription?: string | null;
isPublic?: boolean;
maxParticipants?: number | null;
};
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);
}
export async function listMyCustomChallenges(
params?: { page?: number; pageSize?: number; state?: ChallengeState }
): Promise<ChallengeListResponse> {
const searchParams = new URLSearchParams();
if (params?.page) {
searchParams.append('page', String(params.page));
}
if (params?.pageSize) {
searchParams.append('pageSize', String(params.pageSize));
}
if (params?.state) {
searchParams.append('state', params.state);
}
const query = searchParams.toString();
const url = `/challenges/my/created${query ? `?${query}` : ''}`;
return api.get<ChallengeListResponse>(url);
}
export async function createCustomChallenge(
payload: CreateCustomChallengePayload
): Promise<ChallengeDetailDto> {
return api.post<ChallengeDetailDto>('/challenges/custom', payload);
}
export async function joinChallengeByCode(shareCode: string): Promise<ChallengeProgressDto> {
return api.post<ChallengeProgressDto>('/challenges/join-by-code', { shareCode });
}
export async function getChallengeByShareCode(shareCode: string): Promise<ChallengeDetailDto> {
return api.get<ChallengeDetailDto>(`/challenges/share/${encodeURIComponent(shareCode)}`);
}
export async function regenerateChallengeShareCode(
id: string
): Promise<{ shareCode: string }> {
return api.post<{ shareCode: string }>(
`/challenges/custom/${encodeURIComponent(id)}/regenerate-code`
);
}