@cs2dak/react 0.2.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,178 +0,0 @@
1
- import type { DemoViewModel } from "@cs2dak/contract";
2
- import { Activity, Crosshair, Gauge, ListChecks, Map, ShieldAlert, Table2 } from "lucide-react";
3
- import { useMemo, useState, type ReactNode } from "react";
4
- import { EconomyPanel } from "./EconomyPanel";
5
- import { HeatmapCanvas } from "./HeatmapCanvas";
6
- import { QaReportPanel } from "./QaReportPanel";
7
- import { RoundTimeline } from "./RoundTimeline";
8
- import { ScoreboardTable } from "./ScoreboardTable";
9
-
10
- export interface DemoAnalysisDashboardProps {
11
- model: DemoViewModel;
12
- }
13
-
14
- type ViewKey = "overview" | "rounds" | "spatial" | "qa";
15
-
16
- export function DemoAnalysisDashboard({ model }: DemoAnalysisDashboardProps) {
17
- const [view, setView] = useState<ViewKey>("overview");
18
- const topPlayer = model.scoreboard[0];
19
- const economySwing = useMemo(
20
- () => model.economy.reduce((max, point) => Math.max(max, Math.abs(point.advantage)), 0),
21
- [model.economy]
22
- );
23
-
24
- return (
25
- <main className="dak-shell">
26
- <div className="dak-workspace">
27
- <header className="dak-header">
28
- <div>
29
- <div className="dak-eyebrow">CS2 Demo Analysis Kit</div>
30
- <h1 className="dak-title">{model.title}</h1>
31
- <p className="dak-subtitle">{model.subtitle}</p>
32
- </div>
33
- <div className="dak-scoreblock">
34
- <div className="dak-scoreline">{model.scoreline}</div>
35
- <div className="dak-mapline">{model.map.name}</div>
36
- </div>
37
- </header>
38
-
39
- <section className="dak-kpi-strip">
40
- <Metric icon={<Gauge size={16} />} label="最高 V2 RR" value={topPlayer ? topPlayer.accountRR.toFixed(3) : "0.000"} detail={topPlayer?.name ?? "暂无选手"} />
41
- <Metric icon={<Crosshair size={16} />} label="最高 ADR" value={topPlayer ? topPlayer.adr.toFixed(1) : "0.0"} detail="来自伤害统计" />
42
- <Metric icon={<Activity size={16} />} label="最大装备差" value={`$${economySwing.toLocaleString()}`} detail={`${model.economy.length} 回合`} />
43
- <Metric icon={<ShieldAlert size={16} />} label="数据检查" value={model.qa.summary.issueCount.toString()} detail={model.qa.ok ? "通过" : "需要检查"} />
44
- </section>
45
-
46
- <nav className="dak-tabs" aria-label="Analysis views">
47
- <TabButton active={view === "overview"} icon={<Table2 size={16} />} label="总览" onClick={() => setView("overview")} />
48
- <TabButton active={view === "rounds"} icon={<ListChecks size={16} />} label="回合" onClick={() => setView("rounds")} />
49
- <TabButton active={view === "spatial"} icon={<Map size={16} />} label="地图" onClick={() => setView("spatial")} />
50
- <TabButton active={view === "qa"} icon={<ShieldAlert size={16} />} label="检查" onClick={() => setView("qa")} />
51
- </nav>
52
-
53
- {view === "overview" && (
54
- <div className="dak-grid">
55
- <section className="dak-stack">
56
- <Panel title="选手数据 / RR">
57
- <ScoreboardTable rows={model.scoreboard} />
58
- </Panel>
59
- <Panel title="经济走势">
60
- <EconomyPanel
61
- points={model.economy}
62
- teamAName={model.teams.teamA.name}
63
- teamBName={model.teams.teamB.name}
64
- />
65
- </Panel>
66
- </section>
67
-
68
- <aside className="dak-stack">
69
- <Panel title="地图热力图">
70
- <HeatmapCanvas map={model.map} points={model.heatmap} />
71
- </Panel>
72
- <Panel title="回合事件">
73
- <RoundTimeline events={model.timeline} />
74
- </Panel>
75
- </aside>
76
- </div>
77
- )}
78
-
79
- {view === "rounds" && (
80
- <div className="dak-grid dak-grid-even">
81
- <Panel title="回合事件">
82
- <RoundTimeline events={model.timeline} />
83
- </Panel>
84
- <Panel title="选手回合明细">
85
- <div className="dak-roundfact-list">
86
- {model.playerRoundFacts.slice(0, 80).map((fact) => (
87
- <div className="dak-roundfact-row" key={`${fact.roundNumber}-${fact.steamId64}`}>
88
- <span className="dak-badge">R{fact.roundNumber}</span>
89
- <span>{fact.name}</span>
90
- <span className="dak-muted">{sideLabel(fact.side)} · {economyLabel(fact.economyType)}</span>
91
- <span className="dak-mono">{fact.kills}/{fact.deaths}/{fact.assists}</span>
92
- <span className="dak-tags">{fact.kastTags.length > 0 ? fact.kastTags.map(kastLabel).join("、") : "无贡献回合"}</span>
93
- </div>
94
- ))}
95
- </div>
96
- </Panel>
97
- </div>
98
- )}
99
-
100
- {view === "spatial" && (
101
- <div className="dak-grid dak-grid-even">
102
- <Panel title="热力图">
103
- <HeatmapCanvas map={model.map} points={model.heatmap} />
104
- </Panel>
105
- <Panel title="经济走势">
106
- <EconomyPanel points={model.economy} teamAName={model.teams.teamA.name} teamBName={model.teams.teamB.name} />
107
- </Panel>
108
- </div>
109
- )}
110
-
111
- {view === "qa" && (
112
- <Panel title="数据检查">
113
- <QaReportPanel report={model.qa} />
114
- </Panel>
115
- )}
116
- </div>
117
- </main>
118
- );
119
- }
120
-
121
- function sideLabel(side: string) {
122
- return side === "t" ? "进攻方" : "防守方";
123
- }
124
-
125
- function economyLabel(type: string | null) {
126
- const labels: Record<string, string> = {
127
- pistol: "手枪局",
128
- eco: "ECO",
129
- semi: "半起",
130
- force: "强起",
131
- full: "长枪",
132
- conversion: "长枪"
133
- };
134
- return type ? labels[type] ?? type : "未知经济";
135
- }
136
-
137
- function kastLabel(tag: string) {
138
- const labels: Record<string, string> = {
139
- kill: "击杀",
140
- assist: "助攻",
141
- survive: "存活",
142
- trade: "被补枪"
143
- };
144
- return labels[tag] ?? tag;
145
- }
146
-
147
- function Metric({ icon, label, value, detail }: { icon: ReactNode; label: string; value: string; detail: string }) {
148
- return (
149
- <div className="dak-metric">
150
- <div className="dak-metric-icon">{icon}</div>
151
- <div>
152
- <div className="dak-metric-label">{label}</div>
153
- <div className="dak-metric-value">{value}</div>
154
- <div className="dak-metric-detail">{detail}</div>
155
- </div>
156
- </div>
157
- );
158
- }
159
-
160
- function TabButton({ active, icon, label, onClick }: { active: boolean; icon: ReactNode; label: string; onClick: () => void }) {
161
- return (
162
- <button className={active ? "dak-tab dak-tab-active" : "dak-tab"} type="button" onClick={onClick}>
163
- {icon}
164
- <span>{label}</span>
165
- </button>
166
- );
167
- }
168
-
169
- function Panel({ title, children }: { title: string; children: ReactNode }) {
170
- return (
171
- <section className="dak-panel">
172
- <div className="dak-panel-header">
173
- <h2 className="dak-panel-title">{title}</h2>
174
- </div>
175
- <div className="dak-panel-body">{children}</div>
176
- </section>
177
- );
178
- }
@@ -1,54 +0,0 @@
1
- import type { EconomyConversion } from "@cs2dak/core";
2
- import { economyLabelCn } from "@cs2dak/core";
3
-
4
- export interface EconomyConversionPanelProps {
5
- /** One team's per-economy-type conversion (from buildEconomyConversion). */
6
- stats: EconomyConversion;
7
- teamName: string;
8
- }
9
-
10
- export function EconomyConversionPanel({ stats, teamName }: EconomyConversionPanelProps) {
11
- const entries = Object.entries(stats).sort(([, a], [, b]) => b.winRate - a.winRate);
12
-
13
- if (entries.length === 0) {
14
- return (
15
- <div className="dak-econ-conv" aria-label={`${teamName} 经济转化率`}>
16
- <p className="dak-empty">暂无经济数据</p>
17
- </div>
18
- );
19
- }
20
-
21
- return (
22
- <div className="dak-econ-conv" aria-label={`${teamName} 经济转化率`}>
23
- <table className="dak-table">
24
- <thead>
25
- <tr>
26
- <th>经济类型</th>
27
- <th>回合</th>
28
- <th>胜</th>
29
- <th>胜率</th>
30
- <th className="dak-econ-conv-bar-col">胜率条</th>
31
- </tr>
32
- </thead>
33
- <tbody>
34
- {entries.map(([economy, s]) => (
35
- <tr key={economy}>
36
- <td>{economyLabelCn(economy)}</td>
37
- <td className="dak-mono">{s.played}</td>
38
- <td className="dak-mono">{s.won}</td>
39
- <td className="dak-mono">{(s.winRate * 100).toFixed(1)}%</td>
40
- <td className="dak-econ-conv-bar-col">
41
- <div className="dak-winrate-bar">
42
- <div
43
- className="dak-winrate-fill"
44
- style={{ width: `${Math.round(s.winRate * 100)}%` }}
45
- />
46
- </div>
47
- </td>
48
- </tr>
49
- ))}
50
- </tbody>
51
- </table>
52
- </div>
53
- );
54
- }