feat(challenges): 新增挑战详情页与排行榜及轮播卡片交互

- 重构挑战列表为横向轮播,支持多进行中的挑战
- 新增挑战详情页 /challenges/[id]/index 与排行榜 /challenges/[id]/leaderboard
- ChallengeProgressCard 支持小时级剩余时间显示
- 新增 ChallengeRankingItem 组件展示榜单项
- 排行榜支持分页加载、下拉刷新与错误重试
- 挑战卡片新增已结束角标与渐变遮罩
- 加入/退出挑战时展示庆祝动画与错误提示
- 统一背景渐变色与卡片阴影细节
This commit is contained in:
richarjiang
2025-09-30 11:33:24 +08:00
parent d32a822604
commit b0602b0a99
8 changed files with 871 additions and 159 deletions

View File

@@ -20,6 +20,11 @@ type ChallengeProgressCardProps = {
inactiveColor?: string;
};
type RemainingTime = {
value: number;
unit: '天' | '小时';
};
const DEFAULT_BACKGROUND: [string, string] = ['#ffffff', '#ffffff'];
const DEFAULT_TITLE_COLOR = '#1c1f3a';
const DEFAULT_SUBTITLE_COLOR = '#707baf';
@@ -38,11 +43,22 @@ const clampSegments = (target: number, completed: number) => {
return { segmentsCount, completedSegments };
};
const calculateRemainingDays = (endAt?: string) => {
if (!endAt) return 0;
const calculateRemainingTime = (endAt?: string): RemainingTime => {
if (!endAt) return { value: 0, unit: '天' };
const endDate = dayjs(endAt);
if (!endDate.isValid()) return 0;
return Math.max(0, endDate.diff(dayjs(), 'd'));
if (!endDate.isValid()) return { value: 0, unit: '天' };
const diffMilliseconds = endDate.diff(dayjs());
if (diffMilliseconds <= 0) {
return { value: 0, unit: '天' };
}
const diffHours = diffMilliseconds / (60 * 60 * 1000);
if (diffHours < 24) {
return { value: Math.max(1, Math.floor(diffHours)), unit: '小时' };
}
return { value: Math.floor(diffHours / 24), unit: '天' };
};
export const ChallengeProgressCard: React.FC<ChallengeProgressCardProps> = ({
@@ -96,7 +112,7 @@ export const ChallengeProgressCard: React.FC<ChallengeProgressCardProps> = ({
});
}, [segments?.completedSegments, segments?.segmentsCount]);
const remainingDays = useMemo(() => calculateRemainingDays(endAt), [endAt]);
const remainingTime = useMemo(() => calculateRemainingTime(endAt), [endAt]);
if (!hasValidProgress || !progress || !segments) {
return null;
@@ -111,7 +127,9 @@ export const ChallengeProgressCard: React.FC<ChallengeProgressCardProps> = ({
{title}
</Text>
</View>
<Text style={[styles.remaining, { color: subtitleColor }]}> {remainingDays} </Text>
<Text style={[styles.remaining, { color: subtitleColor }]}>
{remainingTime.value} {remainingTime.unit}
</Text>
</View>
<View style={styles.metaRow}>