feat(challenges): 添加自定义挑战功能和多语言支持

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

View File

@@ -2,11 +2,24 @@ 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 = {
@@ -38,7 +51,7 @@ export type ChallengeListItemDto = {
durationLabel: string;
requirementLabel: string;
unit?: string;
status: ChallengeStatus;
status?: ChallengeStatus;
participantsCount: number;
rankingDescription?: string;
highlightTitle: string;
@@ -50,12 +63,23 @@ export type ChallengeListItemDto = {
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;
rankings: RankingItemDto[];
summary?: string | null;
rankings?: RankingItemDto[];
userRank?: number;
challengeState?: ChallengeState;
};
export type ChallengeRankingsDto = {
@@ -65,6 +89,31 @@ export type ChallengeRankingsDto = {
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');
}
@@ -101,3 +150,43 @@ export async function getChallengeRankings(
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`
);
}