forked from wangziqi/gongxue-base
feat: add taro student learning flow
This commit is contained in:
151
apps/taro/src/pages/student/profile/index.tsx
Normal file
151
apps/taro/src/pages/student/profile/index.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { Button, Input, Text, View } from '@tarojs/components';
|
||||
import { logout } from '@/services/auth';
|
||||
import { checkActivationCode, loadEntitlements, loadOrders, loadSvipPlans, redeemActivationCode, type OrderItem, type SvipPlan } from '@/services/commerce';
|
||||
import { loadLeaderboard } from '@/services/learning';
|
||||
import { checkIn, loadBadges, loadExamCountdowns, loadProfile, type StudentProfile } from '@/services/profile';
|
||||
import '../student.css';
|
||||
|
||||
export default function StudentProfilePage() {
|
||||
const [profile, setProfile] = useState<StudentProfile | null>(null);
|
||||
const [plans, setPlans] = useState<SvipPlan[]>([]);
|
||||
const [orders, setOrders] = useState<OrderItem[]>([]);
|
||||
const [badges, setBadges] = useState<Record<string, unknown>[]>([]);
|
||||
const [countdowns, setCountdowns] = useState<Record<string, unknown>[]>([]);
|
||||
const [leaderboard, setLeaderboard] = useState<Record<string, unknown>[]>([]);
|
||||
const [activationCode, setActivationCode] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
function reload() {
|
||||
Promise.all([
|
||||
loadProfile().catch(() => ({ item: null })),
|
||||
loadSvipPlans().catch(() => ({ items: [] })),
|
||||
loadOrders().catch(() => ({ items: [] })),
|
||||
loadBadges().catch(() => ({ items: [] })),
|
||||
loadExamCountdowns().catch(() => ({ items: [] })),
|
||||
loadLeaderboard('questions').catch(() => ({ items: [] })),
|
||||
loadEntitlements().catch(() => ({ summary: {} })),
|
||||
]).then(([profilePayload, planPayload, orderPayload, badgePayload, countdownPayload, leaderboardPayload]) => {
|
||||
setProfile(profilePayload.item || null);
|
||||
setPlans(planPayload.items || []);
|
||||
setOrders(orderPayload.items || []);
|
||||
setBadges(badgePayload.items || []);
|
||||
setCountdowns(countdownPayload.items || []);
|
||||
setLeaderboard(leaderboardPayload.items || []);
|
||||
}).catch(nextError => setError(nextError instanceof Error ? nextError.message : '个人中心加载失败'));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload();
|
||||
}, []);
|
||||
|
||||
async function handleCheckIn() {
|
||||
try {
|
||||
const payload = await checkIn();
|
||||
setMessage(`签到成功,当前积分 ${String(payload.item?.score ?? '')}`);
|
||||
reload();
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '签到失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRedeem() {
|
||||
try {
|
||||
await checkActivationCode(activationCode);
|
||||
await redeemActivationCode(activationCode);
|
||||
setMessage('激活码兑换成功');
|
||||
setActivationCode('');
|
||||
reload();
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '激活码兑换失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
await logout();
|
||||
Taro.redirectTo({ url: '/pages/student/login/index' });
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='student-page'>
|
||||
<View className='student-topbar'>
|
||||
<View className='student-title-block'>
|
||||
<Text className='student-kicker'>Profile</Text>
|
||||
<Text className='student-title'>{profile?.name || profile?.phone || '个人中心'}</Text>
|
||||
<Text className='student-subtitle'>{profile?.membership?.isSvip ? 'SVIP 生效中' : '普通用户'} · 积分 {profile?.score ?? 0}</Text>
|
||||
</View>
|
||||
<Button className='secondary-button' onClick={handleLogout}>退出</Button>
|
||||
</View>
|
||||
|
||||
<View className='grid-two'>
|
||||
<View className='metric'><Text className='metric-value'>{String((profile?.stats?.answers as Record<string, unknown> | undefined)?.totalAnswered ?? 0)}</Text><Text className='metric-label'>累计答题</Text></View>
|
||||
<View className='metric'><Text className='metric-value'>{String((profile?.stats?.vocabulary as Record<string, unknown> | undefined)?.masteredWords ?? 0)}</Text><Text className='metric-label'>掌握单词</Text></View>
|
||||
</View>
|
||||
|
||||
<View className='section-block'>
|
||||
<Text className='section-heading'>目标与考试</Text>
|
||||
<View className='list-stack'>
|
||||
<View className='list-row'>
|
||||
<Text className='row-main'>{profile?.target?.regionName || '未选择地区'}{profile?.target?.schoolName ? ` · ${profile.target.schoolName}` : ''}</Text>
|
||||
<Text className='row-meta'>{profile?.target?.majorName || '暂无目标专业'}</Text>
|
||||
</View>
|
||||
{countdowns.slice(0, 3).map(item => (
|
||||
<View className='list-row' key={String(item.id || item.examName)}>
|
||||
<Text className='row-main'>{String(item.examName || '考试')}</Text>
|
||||
<Text className='row-meta'>剩余 {String(item.daysLeft ?? '-')} 天</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='section-block'>
|
||||
<Text className='section-heading'>会员套餐</Text>
|
||||
<View className='list-stack'>
|
||||
{plans.slice(0, 4).map(item => (
|
||||
<View className='list-row' key={item.id}>
|
||||
<Text className='row-main'>{item.name} · ¥{String(item.price ?? 0)}</Text>
|
||||
<Text className='row-meta'>{item.days === -1 ? '永久' : `${item.days || 0} 天`} {item.badge ? ` · ${item.badge}` : ''}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='section-block'>
|
||||
<Text className='section-heading'>激活码</Text>
|
||||
<View className='toolbar'>
|
||||
<Input className='input' placeholder='输入激活码' value={activationCode} onInput={event => setActivationCode(String(event.detail.value || ''))} />
|
||||
<Button className='primary-button' onClick={handleRedeem}>兑换</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='section-block'>
|
||||
<Text className='section-heading'>订单</Text>
|
||||
<View className='list-stack'>
|
||||
{orders.slice(0, 5).map(item => (
|
||||
<View className='list-row' key={item.id}>
|
||||
<Text className='row-main'>{item.productName || item.orderNo}</Text>
|
||||
<Text className='row-meta'>{item.status} · ¥{((item.amountCents || 0) / 100).toFixed(2)}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='section-block'>
|
||||
<Text className='section-heading'>运营数据</Text>
|
||||
<View className='toolbar'>
|
||||
<Button className='primary-button' onClick={handleCheckIn}>签到</Button>
|
||||
<Button className='secondary-button' onClick={() => Taro.navigateTo({ url: '/pages/student/catalog/index' })}>刷题</Button>
|
||||
</View>
|
||||
<View className='grid-two'>
|
||||
<View className='metric'><Text className='metric-value'>{String(badges.length)}</Text><Text className='metric-label'>勋章</Text></View>
|
||||
<View className='metric'><Text className='metric-value'>{String(leaderboard.length)}</Text><Text className='metric-label'>排行榜样本</Text></View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{message ? <Text className='success-text'>{message}</Text> : null}
|
||||
{error ? <Text className='error-text'>{error}</Text> : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user