import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { AttributeRadar } from "../components/AttributeRadar"; import { AppIcon } from "../components/PixelIcons"; import { Card, ErrorState, Loading, Tag } from "../components/ui"; import { usePlayerProgress } from "../hooks/usePlayerProgress"; import { api } from "../lib/api"; import { syncPlayerProgress } from "../lib/learningSync"; import { LEVEL_TITLES, type AchievementCategory, type BranchEpithet, type EpithetBranchKind, } from "../lib/playerProgress"; const EPITHET_BRANCH_ORDER: EpithetBranchKind[] = [ "macro", "pick", "timing", "discipline", "pattern", "strategy", ]; function fmtPct(v: number | null) { if (v == null || Number.isNaN(v)) return "—"; return `${v.toFixed(0)}%`; } const TIER_LABEL = { bronze: "銅", silver: "銀", gold: "金", legend: "傳說" } as const; export default function Profile() { const qc = useQueryClient(); const { player, isLoading, isError } = usePlayerProgress(); const [resetBusy, setResetBusy] = useState(false); const [resetMsg, setResetMsg] = useState(null); const [nameDraft, setNameDraft] = useState(""); const [nameBusy, setNameBusy] = useState(false); const [nameMsg, setNameMsg] = useState(null); const [epithetBusy, setEpithetBusy] = useState(null); const [epithetMsg, setEpithetMsg] = useState(null); useEffect(() => { if (player?.displayName) setNameDraft(player.displayName); }, [player?.displayName]); const equipEpithet = async (id: string | null) => { setEpithetBusy(id ?? "auto"); setEpithetMsg(null); try { const res = await api.setPlayerEpithet(id); syncPlayerProgress(qc); setEpithetMsg(res.message || "已更新稱號"); } catch (e) { setEpithetMsg((e as Error).message || "裝備失敗"); } finally { setEpithetBusy(null); } }; const saveName = async () => { const trimmed = nameDraft.trim(); if (trimmed.length < 2) { setNameMsg("名稱至少 2 個字。"); return; } setNameBusy(true); setNameMsg(null); try { const res = await api.setPlayerName(trimmed); syncPlayerProgress(qc); setNameDraft(res.displayName || trimmed); setNameMsg(res.message || "已儲存"); } catch (e) { setNameMsg((e as Error).message || "儲存失敗"); } finally { setNameBusy(false); } }; const resetCharacter = async () => { const ok = window.confirm( "確定要重置角色養成?\n\n將清空:心法進度、試煉、筆記、圖鑑、EXP/金幣/徽章,以及交易上的復盤欄位。\n交易紀錄與帳號會保留。", ); if (!ok) return; setResetBusy(true); setResetMsg(null); try { const res = await api.resetPlayerCharacter(); syncPlayerProgress(qc); qc.invalidateQueries({ queryKey: ["skill-state"] }); qc.invalidateQueries({ queryKey: ["pattern-state"] }); qc.invalidateQueries({ queryKey: ["trades"] }); qc.invalidateQueries({ queryKey: ["trade-stats"] }); setResetMsg(res.message || "角色已重置。"); } catch (e) { setResetMsg((e as Error).message || "重置失敗"); } finally { setResetBusy(false); } }; if (isLoading) return ; if (isError || !player) return ; const xpPct = player.xpToNext ? Math.round((player.xpInLevel / player.xpToNext) * 100) : 100; const unlockedCount = player.achievements.filter((a) => a.unlocked).length; const questsByChapter = player.quests.reduce>((acc, q) => { (acc[q.chapter] ||= []).push(q); return acc; }, {}); const epithetsByBranch = (player.epithets || []).reduce>((acc, e) => { (acc[e.branch] ||= []).push(e); return acc; }, {}); const achievementsByCategory = player.achievements.reduce< Record >((acc, a) => { (acc[a.category] ||= []).push(a); return acc; }, {} as Record); return ( <>
我的

角色養成

EXP 只獎勵「學懂、驗證、反思」——不看你短期勝率或賺賠。這是投資學習的冒險地圖。

setNameDraft(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") void saveName(); }} />
{nameMsg ? {nameMsg} : null}
Lv.{player.level} · {player.title} {player.epithet ? 「{player.epithet}」 : null}
{player.learningStage}
{player.gold.toLocaleString()} G

{player.learningStageHint}

EXP {player.xpInLevel.toLocaleString()} / {player.xpToNext.toLocaleString()}

累積 {player.totalXp.toLocaleString()} EXP

}>

主稱號只看 Lv,代表你的冒險資歷。

{LEVEL_TITLES.map((t) => { const unlocked = player.level >= t.minLevel; const current = player.title === t.title; return (
Lv.{t.minLevel} {t.title} {current ? 目前 : unlocked ? 已達 : 未達}
); })}
}>

五維屬性實戰流派解鎖不同稱謂,可自由裝備展示。 {player.dominantAttribute ? ` 目前主屬性:${player.attributes.find((a) => a.id === player.dominantAttribute)?.label || "—"}。` : " 多修煉後會出現主屬性。"}

{player.epithet ? 展示中:{player.epithet} : 尚無支線稱號}
{epithetMsg ?

{epithetMsg}

: null}
} className="mt">

走不同學習路線會解鎖不同稱謂——總經派、選股派、波段派、紀律派、圖鑑派,或實戰流派組合。

{EPITHET_BRANCH_ORDER.map((branch) => { const items = epithetsByBranch[branch]; if (!items?.length) return null; return (
{items[0].branchLabel}
{items.map((e) => { const equipped = player.epithetId === e.id; const isAutoEquipped = (player.equippedEpithetId === "auto" || !player.equippedEpithetId) && equipped; return (
{e.label}
{e.desc}
{e.requirement}
{!e.unlocked && e.progress ? (
{e.progress}
) : null}
{e.unlocked ? ( ) : ( 未解鎖 )}
); })}
); })}
} className="mt" >

設計原則:理解 > 勝率反思 > 交易次數精通 > 瀏覽

{player.xpBreakdown.map((line) => (
{line.label} +{line.xp.toLocaleString()}

{line.hint}

))}
}>

每項滿分 100,由底下可核對的計分項加總。點開可看公式與你的實際數字。

{player.attributes.map((attr) => (
{attr.label}
{attr.value}

{attr.hint}

計分項 {attr.earned}/{attr.max} 分
{attr.breakdown.map((line) => (
{line.label} {line.earned}/{line.max} {line.detail}
))}
))}
}>
心法解鎖 {player.skillStats.read} / {player.skillStats.total}
心法精通 {player.skillStats.mastered} 條
線型圖鑑 {player.patternStats.collected} / {player.patternStats.total}
完整復盤 {player.journalStats.withReflection} 筆
心法標註交易 {player.journalStats.withPrinciple} 筆
多元心法復盤 {player.journalStats.distinctPrinciples} 種
無犯錯率 {fmtPct(player.journalStats.discipline)}
心法技能樹 線型圖鑑 復盤戰績
} className="mt" >

依你目前的階段排列;每章都在回答「下一步該學什麼、為什麼」。

{Object.entries(questsByChapter).map(([chapter, items]) => (
{chapter}
{items.map((q) => (
{q.done ? "✓" : ""}
{q.title} {q.progress ? {q.progress} : null}
{q.sub}
{q.why}
+{q.xp} EXP {q.href && !q.done ? ( 前往 ) : null}
))}
))}
} className="mt" >
{player.achievementCategories.map((c) => ( {c.label} {c.unlocked}/{c.total} ))}
{player.achievementCategories.map((cat) => { const items = achievementsByCategory[cat.id] || []; if (!items.length) return null; return (
{cat.label} {cat.unlocked}/{cat.total}
{items.map((a) => (
{a.name} {a.tier ? ( {TIER_LABEL[a.tier]} ) : null}
{a.desc}
))}
); })}
} className="mt">

從 Lv.1 重新開始學習路線。不會刪除交易帳號與買賣紀錄,但會清掉復盤心得、心法標註等養成資料。

{resetMsg ?

{resetMsg}

: null}
} className="mt"> } className="mt">

從 Lv.1 重新開始學習路線。不會刪除交易帳號與買賣紀錄,但會清掉復盤心得、心法標註等養成資料。

{resetMsg ?

{resetMsg}

: null}
); } function ContentReadingStats() { const q = useQuery({ queryKey: ["content-stats"], queryFn: () => api.contentStats(), staleTime: 30000, }); if (!q.data) return null; const stats = q.data; const total = stats.total || 0; if (!total) return

尚未從 YouTube 或 HyRead 匯入內容

; return (
YouTube 影片{stats.byKind?.youtube?.ready || 0} 部
HyRead 書籍{stats.byKind?.hyread?.ready || 0} 本
知識庫總計{total} 個來源
); }