import { useEffect, useState, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import ReactEChartsCore from 'echarts-for-react/lib/core'; import * as echarts from 'echarts/core'; import { LineChart } from 'echarts/charts'; import { GridComponent, TooltipComponent } from 'echarts/components'; import { CanvasRenderer } from 'echarts/renderers'; import { useAuthStore } from '../store/auth'; import { profileApi, authApi } from '../lib/api'; import type { ProfileOverview, AdminRecord } from '../types'; import { showToast } from '../components/Toast'; import styles from './ProfilePage.module.css'; import { AxiosError } from 'axios'; echarts.use([LineChart, GridComponent, TooltipComponent, CanvasRenderer]); export function ProfilePage() { const user = useAuthStore((s) => s.user); const logout = useAuthStore((s) => s.logout); const navigate = useNavigate(); const [overview, setOverview] = useState(null); const [records, setRecords] = useState([]); const [recordsTotal, setRecordsTotal] = useState(0); const [recordsPage, setRecordsPage] = useState(1); const [trendPeriod, setTrendPeriod] = useState<'7d' | '30d'>('7d'); const [loading, setLoading] = useState(true); const [pwModalOpen, setPwModalOpen] = useState(false); const [oldPw, setOldPw] = useState(''); const [newPw, setNewPw] = useState(''); const [confirmPw, setConfirmPw] = useState(''); const [pwError, setPwError] = useState(''); const [pwSaving, setPwSaving] = useState(false); const fetchOverview = useCallback(async () => { try { const { data } = await profileApi.getOverview(trendPeriod); setOverview(data); } catch { showToast('加载消费概览失败'); } }, [trendPeriod]); const fetchRecords = useCallback(async () => { try { const { data } = await profileApi.getRecords(recordsPage, 20); if (recordsPage === 1) { setRecords(data.results); } else { setRecords((prev) => [...prev, ...data.results]); } setRecordsTotal(data.total); } catch { showToast('加载消费记录失败'); } }, [recordsPage]); useEffect(() => { Promise.all([fetchOverview(), fetchRecords()]).finally(() => setLoading(false)); }, []); useEffect(() => { fetchOverview(); }, [fetchOverview]); useEffect(() => { fetchRecords(); }, [fetchRecords]); const handleLogout = () => { logout(); navigate('/login', { replace: true }); }; const handleChangePassword = async () => { setPwError(''); if (!oldPw) { setPwError('请输入旧密码'); return; } if (newPw.length < 8) { setPwError('新密码至少8位'); return; } if (newPw !== confirmPw) { setPwError('两次输入的新密码不一致'); return; } setPwSaving(true); try { await authApi.changePassword(oldPw, newPw); showToast('密码修改成功,请重新登录'); setPwModalOpen(false); setOldPw(''); setNewPw(''); setConfirmPw(''); setTimeout(() => { logout(); navigate('/login', { replace: true }); }, 1500); } catch (err) { const msg = (err as AxiosError<{ message?: string }>)?.response?.data?.message || '修改失败'; setPwError(msg); } finally { setPwSaving(false); } }; if (loading || !overview) { return (
); } const dailyGenLimit = overview.daily_generation_limit || 0; const dailyGenUsed = overview.daily_generation_used || 0; const monthlyGenLimit = overview.monthly_generation_limit || 0; const monthlyGenUsed = overview.monthly_generation_used || 0; const dailyPercent = dailyGenLimit > 0 ? (dailyGenUsed / dailyGenLimit) * 100 : 0; const monthlyPercent = monthlyGenLimit > 0 ? (monthlyGenUsed / monthlyGenLimit) * 100 : 0; const sparklineOption: echarts.EChartsCoreOption = { tooltip: { trigger: 'axis', backgroundColor: '#1e1e2a', borderColor: '#2a2a38', textStyle: { color: '#e2e8f0', fontSize: 12 }, }, grid: { left: 0, right: 0, top: 5, bottom: 0, containLabel: false }, xAxis: { type: 'category', show: false, data: overview.daily_trend.map((d) => d.date.slice(5)) }, yAxis: { type: 'value', show: false }, series: [{ type: 'line', data: overview.daily_trend.map((d) => d.seconds), smooth: true, symbol: 'none', lineStyle: { color: '#00b8e6', width: 2 }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: 'rgba(0, 184, 230, 0.3)' }, { offset: 1, color: 'rgba(0, 184, 230, 0.02)' }, ]), }, }], }; const statusMap: Record = { queued: '排队中', processing: '生成中', completed: '已完成', failed: '失败' }; return (

个人中心

{user?.username}
{/* Quota warning */} {dailyPercent >= 80 && dailyPercent < 100 && (
今日额度已使用 {dailyPercent.toFixed(0)}%,请合理使用
)} {dailyPercent >= 100 && (
今日额度已用完,请明天再试
)} {/* Consumption Overview */}

消费概览

今日生成
{dailyGenUsed} / {dailyGenLimit === -1 ? '不限' : dailyGenLimit + '次'}
80 ? (dailyPercent >= 100 ? 'var(--color-danger)' : 'var(--color-warning)') : 'var(--color-primary)', }} />
今日消费 ¥{(overview.daily_spent || 0).toFixed(2)}
本月生成
{monthlyGenUsed} / {monthlyGenLimit === -1 ? '不限' : monthlyGenLimit + '次'}
80 ? 'var(--color-warning)' : 'var(--color-primary)', }} />
本月消费 ¥{(overview.monthly_spent || 0).toFixed(2)}
{overview.team && (
团队 — {overview.team.name}
余额: ¥{(overview.team.balance || 0).toFixed(2)}
可用余额 ¥{(overview.team.available_balance || 0).toFixed(2)}
)}
{/* Consumption Trend */}

消费趋势

{/* Consumption Records */}

消费记录

{records.length === 0 ? (
暂无记录
) : ( records.map((r) => (
{new Date(r.created_at).toLocaleString('zh-CN')}
{r.prompt || '-'}
¥{(r.cost_amount || 0).toFixed(2)} {r.mode === 'universal' ? '全能参考' : '首尾帧'} {statusMap[r.status]}
)) )}
{records.length < recordsTotal && ( )}
{pwModalOpen && (
{ if (e.target === e.currentTarget) setPwModalOpen(false); }}>

修改密码

setOldPw(e.target.value)} placeholder="请输入当前密码" />
setNewPw(e.target.value)} placeholder="至少8位" />
setConfirmPw(e.target.value)} placeholder="再次输入新密码" onKeyDown={(e) => e.key === 'Enter' && handleChangePassword()} />
{pwError &&
{pwError}
}
)}
); }