From 2e0bedb5ffb17e32a43900862002581e94e45c88 Mon Sep 17 00:00:00 2001 From: Jacky Yang Date: Tue, 27 Jan 2026 01:40:56 +0800 Subject: [PATCH] 1.2.3-hotfix: Fixed data center not showing graphs. --- database/L3/L3_Features.sqlite | Bin 405504 -> 405504 bytes web/routes/players.py | 141 +++++++++++- web/services/feature_service.py | 3 +- web/templates/tactics/data.html | 371 +++++++++++++++++++++++++++++-- web/templates/tactics/index.html | 242 +++++++++++++++++++- 5 files changed, 719 insertions(+), 38 deletions(-) diff --git a/database/L3/L3_Features.sqlite b/database/L3/L3_Features.sqlite index 5018f4c660dc790fd7914066c1a15b5f80b79e19..db91e56644cc1c26ca73b609d1ca7c9da1276e6e 100644 GIT binary patch delta 824 zcmZ`%T}V@57(UPUosVP3ZBA`xtNdXQ2o<@hb1GDnWd#K(N$XG6Rjdo6OG9Cllhm}$ zQ+|Pz&_%~6qB_G5C-dj3A86TKP#5cu3NPx`AnL4e66go-&G$Yp@ALCUvz}7#~o132NeM*4oQ+)j8gM!W%Un7GijE*)<^Cs?|AC~)pXxRm4EN0 zPR<1y#9$hekR^$wR_XG%3vTmHx`Xh|E@*l-S@Cy@J43RY&!xb`!6#@X;+Gqr*5Ru7 z;DyiWxu(%-608r@`~8g#jX|HUwyw6Ov2=)Usli^v#rYak5hOUTLqCRvH-L{9RjPDL z_A0eS=@yPh=u}v&#n8e$dxlo#=oawd5)|-E5)C|40+-@%@&}sgf;`bCFPhJ$PXMkp z6y&j@d`KoR@68;89BVh$>zSl4uP2Y<+d}e^*kdAu$vM3AVnk%#W5;c~%`&mtsts1z zgeOWa6hjvS3DOB%7fT~#mf?n&x=GwU7!|Kw)OW7PUN2D=k<_EgWfox(;f&G|*|NK^ z*ma>c|j4S`7#d9kLOZu3oQKN$_nOt3ipJ?mhYlI)f0PUVz*2~%dV&#)ZZXO!yW zVk>{1MBDNXeg5+|Q8g!nM92c;u)%siE2&wXd-FF>w~0>))+0S z?G0O6uMegY#4t^dHkgVJ5m9{lKs}-jLWzVD;!df4NjNz>**Rygb@p1>C)i^R_E;On zLvy2TJ~UqroKJ(1v61qrRFGm9GF31zNs?@>DX~6#PCs2w*5kw|&D16;R%*CZhS7t} zj3>f0L;KN|fnTa=Dn1RS=XS#bdeX zO*Yndz*WpPg9ZEDoME67676^}ItnbYZC8qj_&ye1robNYJPQ=ry4ON;$gEs7*WvKY z_RLZgm)qs^jIPFMPFMgTvB?Qj2<$_2LoL*ZEEQhtG;+8pne4_nh7BS$3Y7-*U5C~9 zu7D*PRTZz|6vjeUNFXU8t19W&p;)woQ4p{EtON)TB>b{tu$V0ohh=C346J4G_Aa=W z1!2+s4Cb6PE!6$a(PNw*3xzdEHmAdWQP)T+0cyoS6Nz)E6CKCM*eW<69-67LF-w29%eA~uyv(g=0 -

Data Center: Comparison

- -
- -
- - + +
+ +
+
+

+ 📊 数据对比中心 (Data Comparison) +

+

拖拽左侧队员至下方区域,或点击搜索添加

- - -
-

Multi-player Radar Chart / Bar Chart Area

+
+
+ + +
+
-
-{% endblock %} \ No newline at end of file + + +
+ + +
+ +
+

+ 对比列表 + 0/5 +

+
+ +
+ + + + +
+
+ + +
+ + +
+ +
+

能力模型对比 (Capability Radar)

+
+ +
+
+ + +
+

基础数据 (Basic Stats)

+
+ + + + + + + + + + + + + + +
PlayerRatingK/DADRKAST
+
+
+
+ + +
+

详细数据对比 (Detailed Stats)

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Metric
Rating (Rating/KD)
KD Ratio
Win Rate (胜率)
First Kill Rate (首杀率)
First Death Rate (首死率)
KAST (贡献率)
RWS (Round Win Share)
Multi-Kill Rate (多杀率)
Headshot Rate (爆头率)
Obj (下包 vs 拆包)
+
+
+ + +
+

地图表现 (Map Performance)

+ +
+ + + + + + + + + + + +
Map
+
+
+ +
+
+
\ No newline at end of file diff --git a/web/templates/tactics/index.html b/web/templates/tactics/index.html index 3f06454..24f172a 100644 --- a/web/templates/tactics/index.html +++ b/web/templates/tactics/index.html @@ -248,14 +248,8 @@
- -
-
-
📊
-

数据对比中心 (Construction)

-

此模块正在开发中...

-
-
+ + {% include 'tactics/data.html' %}
@@ -344,6 +338,15 @@ function tacticsApp() { analysisResult: null, debounceTimer: null, + // Data Center State + dataLineup: [], + dataResult: [], + searchQuery: '', + radarChart: null, + allMaps: ['de_mirage', 'de_inferno', 'de_dust2', 'de_nuke', 'de_ancient', 'de_anubis', 'de_vertigo'], + mapStatsCache: {}, + isDraggingOverData: false, + // Board State currentMap: 'de_mirage', map: null, @@ -372,6 +375,11 @@ function tacticsApp() { }, 300); }); + // Watch Data Lineup + this.$watch('dataLineup', () => { + this.comparePlayers(); + }); + // Init map on first board view, or delay this.$watch('activeTab', value => { if (value === 'board') { @@ -397,10 +405,226 @@ function tacticsApp() { // --- Drag & Drop Generic --- dragStart(event, player) { - event.dataTransfer.setData('text/plain', JSON.stringify(player)); + // Only send essential data to avoid circular references with Alpine proxies + const payload = { + steam_id_64: player.steam_id_64, + username: player.username || player.name, + name: player.name || player.username, + avatar_url: player.avatar_url + }; + event.dataTransfer.setData('text/plain', JSON.stringify(payload)); event.dataTransfer.effectAllowed = 'copy'; }, + // --- Data Center Logic --- + searchPlayer() { + if (!this.searchQuery) return; + const q = this.searchQuery.toLowerCase(); + const found = this.roster.find(p => + (p.username && p.username.toLowerCase().includes(q)) || + (p.steam_id_64 && p.steam_id_64.includes(q)) + ); + if (found) { + this.addToDataLineup(found); + this.searchQuery = ''; + } else { + alert('未找到玩家 (Locally)'); + } + }, + + addToDataLineup(player) { + if (this.dataLineup.some(p => p.steam_id_64 === player.steam_id_64)) { + alert('该选手已在对比列表中'); + return; + } + if (this.dataLineup.length >= 5) { + alert('对比列表已满 (最多5人)'); + return; + } + this.dataLineup.push(player); + }, + + removeFromDataLineup(index) { + this.dataLineup.splice(index, 1); + }, + + clearDataLineup() { + this.dataLineup = []; + }, + + dropData(event) { + this.isDraggingOverData = false; + const data = event.dataTransfer.getData('text/plain'); + if (!data) return; + try { + const player = JSON.parse(data); + this.addToDataLineup(player); + } catch (e) { + console.error("Drop Error:", e); + alert("无法解析拖拽数据"); + } + }, + + comparePlayers() { + if (this.dataLineup.length === 0) { + this.dataResult = []; + if (this.radarChart) { + this.radarChart.data.datasets = []; + this.radarChart.update(); + } + return; + } + + const ids = this.dataLineup.map(p => p.steam_id_64).join(','); + + // 1. Fetch Basic & Radar Stats + fetch('/players/api/batch_stats?ids=' + ids) + .then(res => res.json()) + .then(data => { + this.dataResult = data; + // Use $nextTick to ensure DOM update if needed, but for Chart.js usually direct call is fine. + // However, dataResult is reactive. Let's call update explicitly. + this.$nextTick(() => { + this.updateRadarChart(); + }); + }); + + // 2. Fetch Map Stats + fetch('/players/api/batch_map_stats?ids=' + ids) + .then(res => res.json()) + .then(mapData => { + this.mapStatsCache = mapData; + }); + }, + + getMapStat(sid, mapName) { + if (!this.mapStatsCache[sid]) return null; + return this.mapStatsCache[sid].find(m => m.map_name === mapName); + }, + + getPlayerColor(idx) { + const colors = ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6']; + return colors[idx % colors.length]; + }, + + getRatingColor(rating) { + if (rating >= 1.2) return 'text-red-500'; + if (rating >= 1.05) return 'text-green-600'; + return 'text-gray-500'; + }, + + updateRadarChart() { + // Force destroy to avoid state issues (fullSize error) + if (this.radarChart) { + this.radarChart.destroy(); + this.radarChart = null; + } + + const canvas = document.getElementById('dataRadarChart'); + if (!canvas) return; // Tab might not be visible yet + + // Unwrap proxy if needed + const rawData = JSON.parse(JSON.stringify(this.dataResult)); + + const datasets = rawData.map((p, idx) => { + const color = this.getPlayerColor(idx); + const d = [ + p.radar.BAT || 0, p.radar.PTL || 0, p.radar.HPS || 0, + p.radar.SIDE || 0, p.radar.UTIL || 0, p.radar.STA || 0 + ]; + + return { + label: p.username, + data: d, + borderColor: color, + backgroundColor: color + '20', + borderWidth: 2, + pointRadius: 3 + }; + }); + + // Recreate Chart with Profile-aligned config + const ctx = canvas.getContext('2d'); + this.radarChart = new Chart(ctx, { + type: 'radar', + data: { + labels: ['BAT (火力)', 'PTL (手枪)', 'HPS (抗压)', 'SIDE (阵营)', 'UTIL (道具)', 'STA (稳定)'], + datasets: datasets + }, + options: { + maintainAspectRatio: false, + scales: { + r: { + min: 0, + max: 100, + ticks: { + display: false, // Cleaner look like profile + stepSize: 20 + }, + pointLabels: { + font: { size: 12, weight: 'bold' }, + color: (ctx) => document.documentElement.classList.contains('dark') ? '#cbd5e1' : '#374151' + }, + grid: { + color: (ctx) => document.documentElement.classList.contains('dark') ? 'rgba(51, 65, 85, 0.5)' : 'rgba(229, 231, 235, 0.8)' + }, + angleLines: { + color: (ctx) => document.documentElement.classList.contains('dark') ? 'rgba(51, 65, 85, 0.5)' : 'rgba(229, 231, 235, 0.8)' + } + } + }, + plugins: { + legend: { + position: 'bottom', + labels: { + color: (ctx) => document.documentElement.classList.contains('dark') ? '#fff' : '#000', + usePointStyle: true, + padding: 20 + } + } + } + } + }); + }, + + initRadarChart() { + const canvas = document.getElementById('dataRadarChart'); + if (!canvas) return; // Tab might not be visible yet + + const ctx = canvas.getContext('2d'); + this.radarChart = new Chart(ctx, { + type: 'radar', + data: { + labels: ['BAT (火力)', 'PTL (手枪)', 'HPS (抗压)', 'SIDE (阵营)', 'UTIL (道具)', 'STA (稳定)'], + datasets: [] + }, + options: { + scales: { + r: { + min: 0, + max: 100, + ticks: { display: false, stepSize: 20 }, + pointLabels: { + font: { size: 12, weight: 'bold' }, + color: (ctx) => document.documentElement.classList.contains('dark') ? '#cbd5e1' : '#374151' + }, + grid: { + color: (ctx) => document.documentElement.classList.contains('dark') ? '#334155' : '#e5e7eb' + } + } + }, + plugins: { + legend: { + labels: { + color: (ctx) => document.documentElement.classList.contains('dark') ? '#fff' : '#000' + } + } + }, + maintainAspectRatio: false + } + }); + }, + // --- Analysis Logic --- dropAnalysis(event) { const data = event.dataTransfer.getData('text/plain');