@cs2dak/react 1.0.0 → 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.
@@ -0,0 +1,168 @@
1
+ import type { TeamComparisonModel } from "@cs2dak/presentation";
2
+
3
+ /**
4
+ * 队伍对比(赛前侦察):选 A/B 两队 → 各自跨全部己方比赛的状态对比。
5
+ * 两队**无需互相交手**。纯展示:选队与跳回放都通过回调上报,由容器接数据。
6
+ */
7
+ export function TeamComparisonPanel({
8
+ model,
9
+ onSelectPair,
10
+ onOpenMatch
11
+ }: {
12
+ model: TeamComparisonModel;
13
+ /** 选定 A/B 两队(触发重新聚合);不传则下拉只读。 */
14
+ onSelectPair?: (a: string, b: string) => void;
15
+ /** 打开某场比赛回放。 */
16
+ onOpenMatch?: (matchId: string) => void;
17
+ }) {
18
+ if (model.availableTeams.length < 2) {
19
+ return <div className="dak-empty">至少需要两个队伍的比赛才能生成对比。</div>;
20
+ }
21
+ const selectedA = model.teams[0]?.teamName ?? "";
22
+ const selectedB = model.teams[1]?.teamName ?? "";
23
+
24
+ const picker = (
25
+ <div className="dak-team-picker">
26
+ <TeamSelect
27
+ value={selectedA}
28
+ teams={model.availableTeams}
29
+ exclude={selectedB}
30
+ disabled={!onSelectPair}
31
+ onChange={(next) => onSelectPair?.(next, selectedB)}
32
+ />
33
+ <span className="dak-team-picker-vs">VS</span>
34
+ <TeamSelect
35
+ value={selectedB}
36
+ teams={model.availableTeams}
37
+ exclude={selectedA}
38
+ disabled={!onSelectPair}
39
+ onChange={(next) => onSelectPair?.(selectedA, next)}
40
+ />
41
+ </div>
42
+ );
43
+
44
+ if (model.teams.length !== 2) {
45
+ return (
46
+ <div className="dak-team-compare">
47
+ {picker}
48
+ <div className="dak-empty">选择两个不同的队伍生成对比。</div>
49
+ </div>
50
+ );
51
+ }
52
+ const [a, b] = model.teams;
53
+ return (
54
+ <div className="dak-team-compare">
55
+ {picker}
56
+ <div className="dak-team-compare-grid">
57
+ <TeamSide side={a} onOpenMatch={onOpenMatch} />
58
+ <section className="dak-team-radar" aria-label="队伍差异">
59
+ {model.radar.map((row) => (
60
+ <div key={row.metric} className="dak-team-radar-row">
61
+ <span>{row.label}</span>
62
+ <b>{row.a == null ? "—" : row.a}</b>
63
+ <i />
64
+ <b>{row.b == null ? "—" : row.b}</b>
65
+ <em>{row.delta == null ? "—" : row.delta > 0 ? `+${row.delta}` : row.delta}</em>
66
+ </div>
67
+ ))}
68
+ </section>
69
+ <TeamSide side={b} onOpenMatch={onOpenMatch} />
70
+ </div>
71
+ </div>
72
+ );
73
+ }
74
+
75
+ function TeamSelect({
76
+ value,
77
+ teams,
78
+ exclude,
79
+ disabled,
80
+ onChange
81
+ }: {
82
+ value: string;
83
+ teams: TeamComparisonModel["availableTeams"];
84
+ exclude: string;
85
+ disabled?: boolean;
86
+ onChange: (next: string) => void;
87
+ }) {
88
+ return (
89
+ <select
90
+ className="dak-team-select"
91
+ value={value}
92
+ disabled={disabled}
93
+ onChange={(e) => onChange(e.target.value)}
94
+ aria-label="选择对比队伍"
95
+ >
96
+ {teams.map((team) => (
97
+ <option key={team.name} value={team.name} disabled={team.name === exclude}>
98
+ {team.name}({team.matches} 场)
99
+ </option>
100
+ ))}
101
+ </select>
102
+ );
103
+ }
104
+
105
+ function TeamSide({
106
+ side,
107
+ onOpenMatch
108
+ }: {
109
+ side: TeamComparisonModel["teams"][number];
110
+ onOpenMatch?: (matchId: string) => void;
111
+ }) {
112
+ return (
113
+ <section className="dak-team-side">
114
+ <h3>
115
+ {side.teamName} <small className="dak-muted">{side.matchCount} 场</small>
116
+ </h3>
117
+ <table>
118
+ <thead>
119
+ <tr><th>选手</th><th>RR</th><th>ADR</th><th>KAST</th><th>KPR</th><th>DPR</th></tr>
120
+ </thead>
121
+ <tbody>
122
+ {side.players.slice(0, 8).map((row) => (
123
+ <tr key={row.steamId64}>
124
+ <td>{row.name}</td>
125
+ <td>{fmt(row.rr)}</td>
126
+ <td>{fmt(row.adr)}</td>
127
+ <td>{fmt(row.kast)}</td>
128
+ <td>{fmt(row.kpr)}</td>
129
+ <td>{fmt(row.dpr)}</td>
130
+ </tr>
131
+ ))}
132
+ </tbody>
133
+ </table>
134
+ <div className="dak-team-weapons">
135
+ {side.weaponPreference.slice(0, 5).map((weapon) => (
136
+ <span key={weapon.weapon}>{weapon.label}<b>{weapon.sharePercent.toFixed(1)}%</b></span>
137
+ ))}
138
+ </div>
139
+ <div className="dak-team-economy">
140
+ {side.economyWinRate.map((row) => (
141
+ <span key={row.economyType}>{row.economyType}<b>{row.winRatePercent == null ? "—" : `${row.winRatePercent.toFixed(1)}%`}</b></span>
142
+ ))}
143
+ </div>
144
+ {side.matches.length > 0 && (
145
+ <div className="dak-team-matches">
146
+ {side.matches.map((match) => (
147
+ <button
148
+ key={match.matchId}
149
+ type="button"
150
+ className="dak-team-match"
151
+ disabled={!onOpenMatch}
152
+ onClick={() => onOpenMatch?.(match.matchId)}
153
+ title={`${match.mapName} · vs ${match.opponent} · ${match.roundsWon}-${match.roundsLost}`}
154
+ >
155
+ <span className={match.won ? "dak-team-match-win" : "dak-team-match-loss"}>{match.won ? "胜" : "负"}</span>
156
+ <span className="dak-team-match-opp">vs {match.opponent}</span>
157
+ <small>{match.mapName} · {match.roundsWon}-{match.roundsLost}</small>
158
+ </button>
159
+ ))}
160
+ </div>
161
+ )}
162
+ </section>
163
+ );
164
+ }
165
+
166
+ function fmt(value: number | null): string {
167
+ return value == null ? "—" : value.toFixed(value >= 10 ? 1 : 2);
168
+ }
package/src/index.ts CHANGED
@@ -1,13 +1,29 @@
1
- export { MatchWorkspace } from "./components/MatchWorkspace";
1
+ export { MatchWorkspace, ReplayViewer } from "./components/MatchWorkspace";
2
2
  export { ScoreboardTable } from "./components/ScoreboardTable";
3
3
  export { RoundTimeline } from "./components/RoundTimeline";
4
4
  export { EconomyPanel } from "./components/EconomyPanel";
5
5
  export { HeatmapCanvas } from "./components/HeatmapCanvas";
6
+ export { RadarFieldCanvas, type RadarFieldCanvasProps } from "./components/RadarFieldCanvas";
6
7
  export { KillFeed } from "./components/KillFeed";
7
8
  export { QaReportPanel } from "./components/QaReportPanel";
8
9
  export { SeasonLeaderboard } from "./components/SeasonLeaderboard";
10
+ export { TeamComparisonPanel } from "./components/TeamComparisonPanel";
11
+ export { Pagination } from "./components/Pagination";
12
+ export { EmptyState, EvidenceLink, FindingPanel, LimitNote, MetricInfo } from "./components/Primitives";
13
+ export { DoubleAwpAnalysisPanel, PlayerMapPoolPanel, PlayerMapRoleProfilePanel, TeamMapRoleMatrixPanel } from "./components/MapRoles";
14
+ export { ElimBracket, SwissBracket } from "./components/EventBracket";
15
+ export {
16
+ DataTable,
17
+ STUDIO_TABLE_CLASSES,
18
+ DAK_TABLE_CLASSES,
19
+ type DataTableColumn,
20
+ type DataTableClasses,
21
+ type DataTableProps,
22
+ type HeatTone,
23
+ type SortDirection,
24
+ useSortable
25
+ } from "./components/DataTable";
9
26
 
10
- // AdminQaWorkspace 和 EconomyConversionPanel 保留在源文件中但不再从公共 API 导出:
27
+ // AdminQaWorkspace 保留在源文件中但不再从公共 API 导出:
11
28
  // 仓库内零消费者,等待 DAK Studio 建设时重新决定公共面。
12
29
  // import { AdminQaWorkspace } from "./components/MatchWorkspace";
13
- // import { EconomyConversionPanel } from "./components/EconomyConversionPanel";