@cs2dak/react 0.2.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.
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/package.json +26 -0
- package/src/components/DemoAnalysisDashboard.tsx +178 -0
- package/src/components/EconomyConversionPanel.tsx +54 -0
- package/src/components/EconomyPanel.tsx +158 -0
- package/src/components/HeatmapCanvas.tsx +351 -0
- package/src/components/KillFeed.tsx +42 -0
- package/src/components/MatchWorkspace.test.ts +110 -0
- package/src/components/MatchWorkspace.tsx +643 -0
- package/src/components/QaReportPanel.tsx +31 -0
- package/src/components/RoundTimeline.tsx +35 -0
- package/src/components/ScoreboardTable.test.ts +47 -0
- package/src/components/ScoreboardTable.tsx +80 -0
- package/src/index.ts +9 -0
- package/src/theme.css +1721 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Starfie1d
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @cs2dak/react
|
|
2
|
+
|
|
3
|
+
English | 简体中文
|
|
4
|
+
|
|
5
|
+
Previewable React components for `DemoViewModel`. Components are product-neutral and styled through `theme.css`, using a RivalHub-inspired dark tactical language without importing RivalHub code.
|
|
6
|
+
|
|
7
|
+
面向 `DemoViewModel` 的可预览 React 组件。组件不绑定 RivalHub、Next.js 或数据库,默认主题参考 RivalHub 的深色赛事运营设计语言。
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cs2dak/react",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./theme.css": "./src/theme.css"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"lucide-react": "^0.468.0",
|
|
12
|
+
"@cs2dak/core": "0.2.0",
|
|
13
|
+
"@cs2dak/contract": "0.2.0",
|
|
14
|
+
"@cs2dak/maps": "0.2.0"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"react": ">=18",
|
|
18
|
+
"react-dom": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { EconomyPoint } from "@cs2dak/contract";
|
|
2
|
+
|
|
3
|
+
export interface EconomyPanelProps {
|
|
4
|
+
points: EconomyPoint[];
|
|
5
|
+
teamAName: string;
|
|
6
|
+
teamBName: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const width = 760;
|
|
10
|
+
const height = 260;
|
|
11
|
+
const pad = { left: 48, right: 18, top: 24, bottom: 34 };
|
|
12
|
+
const economyLabels = {
|
|
13
|
+
pistol: "手枪",
|
|
14
|
+
eco: "Eco",
|
|
15
|
+
semi: "半起",
|
|
16
|
+
force: "强起",
|
|
17
|
+
full: "长枪",
|
|
18
|
+
conversion: "长枪"
|
|
19
|
+
} as const;
|
|
20
|
+
const economyColors = {
|
|
21
|
+
pistol: "rgba(255, 198, 77, 0.18)",
|
|
22
|
+
eco: "rgba(104, 115, 129, 0.18)",
|
|
23
|
+
semi: "rgba(73, 182, 255, 0.16)",
|
|
24
|
+
force: "rgba(255, 122, 33, 0.18)",
|
|
25
|
+
full: "rgba(83, 215, 126, 0.16)",
|
|
26
|
+
conversion: "rgba(83, 215, 126, 0.16)"
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
const upsetLabels: Record<string, string> = {
|
|
30
|
+
eco: "ECO翻",
|
|
31
|
+
semi: "半起翻",
|
|
32
|
+
force: "强起翻"
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const upsetColors: Record<string, string> = {
|
|
36
|
+
eco: "rgba(255,84,114,0.9)",
|
|
37
|
+
semi: "rgba(73,182,255,0.9)",
|
|
38
|
+
force: "rgba(255,122,33,0.9)"
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function getUpsetType(point: EconomyPoint): "eco" | "semi" | "force" | null {
|
|
42
|
+
const winnerEco = point.winnerTeamKey === "teamA" ? point.teamAEconomy : point.teamBEconomy;
|
|
43
|
+
const loserEco = point.winnerTeamKey === "teamA" ? point.teamBEconomy : point.teamAEconomy;
|
|
44
|
+
if (winnerEco !== "eco" && winnerEco !== "semi" && winnerEco !== "force") return null;
|
|
45
|
+
if (loserEco !== "full" && loserEco !== "conversion") return null;
|
|
46
|
+
return winnerEco;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function EconomyPanel({ points, teamAName, teamBName }: EconomyPanelProps) {
|
|
50
|
+
const maxValue = Math.max(1, ...points.flatMap((point) => [point.teamA, point.teamB]));
|
|
51
|
+
const xFor = (index: number) => pad.left + (index / Math.max(points.length - 1, 1)) * (width - pad.left - pad.right);
|
|
52
|
+
const yFor = (value: number) => pad.top + (1 - value / maxValue) * (height - pad.top - pad.bottom);
|
|
53
|
+
const pathFor = (team: "teamA" | "teamB") =>
|
|
54
|
+
points.map((point, index) => `${index === 0 ? "M" : "L"}${xFor(index)},${yFor(point[team])}`).join(" ");
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div style={{ overflowX: "auto" }}>
|
|
58
|
+
<svg className="dak-chart" viewBox={`0 0 ${width} ${height}`} role="img" aria-label="经济走势">
|
|
59
|
+
{points.map((point, index) => (
|
|
60
|
+
<g key={point.roundNumber}>
|
|
61
|
+
<rect
|
|
62
|
+
x={xFor(index) - 8}
|
|
63
|
+
y={pad.top}
|
|
64
|
+
width={16}
|
|
65
|
+
height={(height - pad.top - pad.bottom) / 2}
|
|
66
|
+
fill={economyColors[point.teamAEconomy]}
|
|
67
|
+
/>
|
|
68
|
+
<rect
|
|
69
|
+
x={xFor(index) - 8}
|
|
70
|
+
y={pad.top + (height - pad.top - pad.bottom) / 2}
|
|
71
|
+
width={16}
|
|
72
|
+
height={(height - pad.top - pad.bottom) / 2}
|
|
73
|
+
fill={economyColors[point.teamBEconomy]}
|
|
74
|
+
/>
|
|
75
|
+
<text x={xFor(index)} y={height - 10} textAnchor="middle" fill="var(--dak-fg-dim)" fontSize="10">
|
|
76
|
+
R{point.roundNumber}
|
|
77
|
+
</text>
|
|
78
|
+
</g>
|
|
79
|
+
))}
|
|
80
|
+
{[0, 0.5, 1].map((ratio) => (
|
|
81
|
+
<g key={ratio}>
|
|
82
|
+
<line
|
|
83
|
+
x1={pad.left}
|
|
84
|
+
x2={width - pad.right}
|
|
85
|
+
y1={yFor(maxValue * ratio)}
|
|
86
|
+
y2={yFor(maxValue * ratio)}
|
|
87
|
+
stroke="var(--dak-border)"
|
|
88
|
+
/>
|
|
89
|
+
<text x={pad.left - 8} y={yFor(maxValue * ratio) + 4} textAnchor="end" fill="var(--dak-fg-dim)" fontSize="10">
|
|
90
|
+
{Math.round(maxValue * ratio)}
|
|
91
|
+
</text>
|
|
92
|
+
</g>
|
|
93
|
+
))}
|
|
94
|
+
<path d={pathFor("teamA")} fill="none" stroke="var(--dak-accent)" strokeWidth="3" />
|
|
95
|
+
<path d={pathFor("teamB")} fill="none" stroke="var(--dak-accent-b)" strokeWidth="3" />
|
|
96
|
+
{points.map((point, index) => {
|
|
97
|
+
const upsetType = getUpsetType(point);
|
|
98
|
+
if (!upsetType) return null;
|
|
99
|
+
return (
|
|
100
|
+
<g key={`upset-${point.roundNumber}`}>
|
|
101
|
+
<circle cx={xFor(index)} cy={pad.top / 2} r={5} fill={upsetColors[upsetType]} />
|
|
102
|
+
</g>
|
|
103
|
+
);
|
|
104
|
+
})}
|
|
105
|
+
</svg>
|
|
106
|
+
<UpsetSummary points={points} />
|
|
107
|
+
<div className="dak-muted">
|
|
108
|
+
<span style={{ color: "var(--dak-accent)" }}>{teamAName}</span>
|
|
109
|
+
{" / "}
|
|
110
|
+
<span style={{ color: "var(--dak-accent-b)" }}>{teamBName}</span>
|
|
111
|
+
{" 每回合装备价值;背景色为每队经济类型"}
|
|
112
|
+
</div>
|
|
113
|
+
<div className="dak-economy-legend" aria-label="经济类型图例">
|
|
114
|
+
{(Object.keys(economyLabels) as Array<keyof typeof economyLabels>).map((type) => (
|
|
115
|
+
<span key={type}><i style={{ background: economyColors[type] }} />{economyLabels[type]}</span>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
<div className="dak-economy-rounds">
|
|
119
|
+
{points.map((point) => {
|
|
120
|
+
const upsetType = getUpsetType(point);
|
|
121
|
+
return (
|
|
122
|
+
<div
|
|
123
|
+
className={`dak-economy-round${upsetType ? " dak-economy-round-upset" : ""}`}
|
|
124
|
+
key={point.roundNumber}
|
|
125
|
+
>
|
|
126
|
+
<b>R{point.roundNumber}</b>
|
|
127
|
+
{upsetType && (
|
|
128
|
+
<span className={`dak-upset-badge dak-upset-badge-${upsetType}`}>
|
|
129
|
+
{upsetLabels[upsetType]}
|
|
130
|
+
</span>
|
|
131
|
+
)}
|
|
132
|
+
<span className={point.winnerTeamKey === "teamA" ? "dak-economy-winner" : undefined}>{teamAName}: {economyLabels[point.teamAEconomy]}</span>
|
|
133
|
+
<span className={point.winnerTeamKey === "teamB" ? "dak-economy-winner" : undefined}>{teamBName}: {economyLabels[point.teamBEconomy]}</span>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
})}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function UpsetSummary({ points }: { points: EconomyPoint[] }) {
|
|
143
|
+
const counts = { eco: 0, semi: 0, force: 0 };
|
|
144
|
+
for (const point of points) {
|
|
145
|
+
const t = getUpsetType(point);
|
|
146
|
+
if (t) counts[t]++;
|
|
147
|
+
}
|
|
148
|
+
const total = counts.eco + counts.semi + counts.force;
|
|
149
|
+
if (total === 0) return null;
|
|
150
|
+
return (
|
|
151
|
+
<div className="dak-economy-upsets">
|
|
152
|
+
<span className="dak-panel-eyebrow">翻盘回合</span>
|
|
153
|
+
{counts.eco > 0 && <span className="dak-upset-chip dak-upset-chip-eco">ECO翻 ×{counts.eco}</span>}
|
|
154
|
+
{counts.semi > 0 && <span className="dak-upset-chip dak-upset-chip-semi">半起翻 ×{counts.semi}</span>}
|
|
155
|
+
{counts.force > 0 && <span className="dak-upset-chip dak-upset-chip-force">强起翻 ×{counts.force}</span>}
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|