1.0.4: Updated Tactics-DeepAnalysis
This commit is contained in:
Binary file not shown.
@@ -53,10 +53,39 @@ def api_analyze():
|
|||||||
'adr': total_adr / count if count else 0
|
'adr': total_adr / count if count else 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 4. Map Stats Calculation
|
||||||
|
map_stats = {} # {map_name: {'count': 0, 'wins': 0}}
|
||||||
|
total_shared_matches = len(shared_matches)
|
||||||
|
|
||||||
|
for m in shared_matches:
|
||||||
|
map_name = m['map_name']
|
||||||
|
if map_name not in map_stats:
|
||||||
|
map_stats[map_name] = {'count': 0, 'wins': 0}
|
||||||
|
|
||||||
|
map_stats[map_name]['count'] += 1
|
||||||
|
if m['is_win']:
|
||||||
|
map_stats[map_name]['wins'] += 1
|
||||||
|
|
||||||
|
# Convert to list for frontend
|
||||||
|
map_stats_list = []
|
||||||
|
for k, v in map_stats.items():
|
||||||
|
win_rate = (v['wins'] / v['count'] * 100) if v['count'] > 0 else 0
|
||||||
|
map_stats_list.append({
|
||||||
|
'map_name': k,
|
||||||
|
'count': v['count'],
|
||||||
|
'wins': v['wins'],
|
||||||
|
'win_rate': win_rate
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort by count desc
|
||||||
|
map_stats_list.sort(key=lambda x: x['count'], reverse=True)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'players': player_data,
|
'players': player_data,
|
||||||
'shared_matches': [dict(m) for m in shared_matches],
|
'shared_matches': [dict(m) for m in shared_matches],
|
||||||
'avg_stats': avg_stats
|
'avg_stats': avg_stats,
|
||||||
|
'map_stats': map_stats_list,
|
||||||
|
'total_shared_matches': total_shared_matches
|
||||||
})
|
})
|
||||||
|
|
||||||
# API: Save Board
|
# API: Save Board
|
||||||
|
|||||||
@@ -345,7 +345,6 @@ class StatsService:
|
|||||||
GROUP BY m.match_id
|
GROUP BY m.match_id
|
||||||
HAVING COUNT(DISTINCT mp.steam_id_64) = ?
|
HAVING COUNT(DISTINCT mp.steam_id_64) = ?
|
||||||
ORDER BY m.start_time DESC
|
ORDER BY m.start_time DESC
|
||||||
LIMIT 20
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
args = list(steam_ids)
|
args = list(steam_ids)
|
||||||
|
|||||||
@@ -91,101 +91,154 @@
|
|||||||
<div x-show="activeTab === 'analysis'" class="space-y-6">
|
<div x-show="activeTab === 'analysis'" class="space-y-6">
|
||||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">阵容化学反应分析</h3>
|
<h3 class="text-xl font-bold text-gray-900 dark:text-white">阵容化学反应分析</h3>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div class="flex flex-col space-y-8">
|
||||||
<!-- Drop Zone -->
|
<!-- Drop Zone -->
|
||||||
<div class="bg-white dark:bg-slate-800 p-6 rounded-lg shadow min-h-[300px]"
|
<div class="bg-white dark:bg-slate-800 p-8 rounded-xl shadow-lg min-h-[320px] border border-gray-100 dark:border-slate-700"
|
||||||
@dragover.prevent @drop="dropAnalysis($event)">
|
@dragover.prevent @drop="dropAnalysis($event)">
|
||||||
<h4 class="font-medium text-gray-700 dark:text-gray-300 mb-4 flex justify-between">
|
<h4 class="text-lg font-bold text-gray-800 dark:text-gray-200 mb-6 flex justify-between items-center">
|
||||||
<span x-text="'阵容构建 (' + analysisLineup.length + '/5)'">阵容构建 (0/5)</span>
|
<span class="flex items-center gap-2">
|
||||||
<button @click="clearAnalysis()" class="text-xs text-red-500 hover:underline">清空</button>
|
<span class="bg-yrtv-100 text-yrtv-700 p-1 rounded">🏗️</span>
|
||||||
|
<span x-text="'阵容构建 (' + analysisLineup.length + '/5)'">阵容构建 (0/5)</span>
|
||||||
|
</span>
|
||||||
|
<button @click="clearAnalysis()" class="px-3 py-1.5 bg-red-50 text-red-600 rounded-md hover:bg-red-100 text-sm font-medium transition">清空全部</button>
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-5 gap-4">
|
<div class="grid grid-cols-5 gap-6">
|
||||||
<template x-for="(p, idx) in analysisLineup" :key="p.steam_id_64">
|
<template x-for="(p, idx) in analysisLineup" :key="p.steam_id_64">
|
||||||
<div class="relative bg-gray-50 dark:bg-slate-700 p-2 rounded border border-gray-200 dark:border-slate-600 flex flex-col items-center">
|
<div class="relative group bg-gradient-to-b from-gray-50 to-gray-100 dark:from-slate-700 dark:to-slate-800 p-4 rounded-xl border-2 border-yrtv-200 dark:border-slate-600 flex flex-col items-center justify-center h-48 shadow-sm transition-all duration-200 hover:-translate-y-1 hover:shadow-md">
|
||||||
<button @click="removeFromAnalysis(idx)" class="absolute top-1 right-1 text-red-400 hover:text-red-600">×</button>
|
<button @click="removeFromAnalysis(idx)" class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center opacity-0 group-hover:opacity-100 transition shadow-sm">×</button>
|
||||||
|
|
||||||
<!-- Avatar -->
|
<!-- Avatar -->
|
||||||
<template x-if="p.avatar_url">
|
<template x-if="p.avatar_url">
|
||||||
<img :src="p.avatar_url" class="w-12 h-12 rounded-full mb-2 object-cover">
|
<img :src="p.avatar_url" class="w-20 h-20 rounded-full mb-3 object-cover border-4 border-white dark:border-slate-600 shadow-md">
|
||||||
</template>
|
</template>
|
||||||
<template x-if="!p.avatar_url">
|
<template x-if="!p.avatar_url">
|
||||||
<div class="w-12 h-12 rounded-full mb-2 bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-sm">
|
<div class="w-20 h-20 rounded-full mb-3 bg-white flex items-center justify-center text-yrtv-600 font-bold text-2xl border-4 border-gray-100 dark:border-slate-600 shadow-md">
|
||||||
<span x-text="(p.username || p.name || p.steam_id_64).substring(0, 2).toUpperCase()"></span>
|
<span x-text="(p.username || p.name || p.steam_id_64).substring(0, 2).toUpperCase()"></span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<span class="text-xs font-bold truncate w-full text-center dark:text-white" x-text="p.username || p.name"></span>
|
<span class="text-sm font-bold truncate w-full text-center dark:text-white mb-1" x-text="p.username || p.name"></span>
|
||||||
<span class="text-[10px] text-gray-500" x-text="'R: ' + (p.stats?.basic_avg_rating || 0).toFixed(2)"></span>
|
<div class="px-2.5 py-1 bg-white dark:bg-slate-900 rounded-full text-xs text-gray-500 dark:text-gray-400 shadow-inner border border-gray-100 dark:border-slate-700">
|
||||||
|
Rating: <span class="font-bold text-yrtv-600" x-text="(p.stats?.basic_avg_rating || 0).toFixed(2)"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Empty Slots -->
|
<!-- Empty Slots -->
|
||||||
<template x-for="i in (5 - analysisLineup.length)">
|
<template x-for="i in (5 - analysisLineup.length)">
|
||||||
<div class="border-2 border-dashed border-gray-300 dark:border-slate-600 rounded flex items-center justify-center h-24 text-gray-400 text-xs">
|
<div class="border-2 border-dashed border-gray-300 dark:border-slate-600 rounded-xl flex flex-col items-center justify-center h-48 text-gray-400 text-sm bg-gray-50/30 dark:bg-slate-800/30 hover:bg-gray-50 dark:hover:bg-slate-800 transition cursor-default">
|
||||||
拖拽至此
|
<div class="text-4xl mb-2 opacity-30 text-gray-300">+</div>
|
||||||
|
<span class="opacity-70">拖拽队员</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6">
|
|
||||||
<button @click="analyzeLineup()" :disabled="analysisLineup.length < 1" class="w-full py-2 bg-yrtv-600 text-white rounded hover:bg-yrtv-700 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
||||||
开始分析 (Analyze)
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Results Area -->
|
<!-- Results Area -->
|
||||||
<div class="bg-white dark:bg-slate-800 p-6 rounded-lg shadow">
|
<div class="bg-white dark:bg-slate-800 p-8 rounded-xl shadow-lg min-h-[240px] border border-gray-100 dark:border-slate-700">
|
||||||
<template x-if="!analysisResult">
|
<template x-if="!analysisResult">
|
||||||
<div class="h-full flex items-center justify-center text-gray-400 text-sm">
|
<div class="h-48 flex flex-col items-center justify-center text-gray-400">
|
||||||
请先选择阵容并点击分析
|
<div class="text-5xl mb-4 opacity-20 grayscale">📊</div>
|
||||||
|
<div class="text-lg font-medium text-gray-500">请先构建阵容,系统将自动分析</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="analysisResult">
|
<template x-if="analysisResult">
|
||||||
<div class="space-y-4">
|
<div class="space-y-6">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-end border-b border-gray-100 dark:border-slate-700 pb-4">
|
||||||
<h4 class="font-bold text-gray-900 dark:text-white">综合评分</h4>
|
<h4 class="font-bold text-xl text-gray-900 dark:text-white flex items-center gap-2">
|
||||||
<span class="text-2xl font-bold text-yrtv-600" x-text="analysisResult.avg_stats.rating.toFixed(2)"></span>
|
<span>📈</span> 综合评分
|
||||||
|
</h4>
|
||||||
|
<div class="flex items-baseline gap-2">
|
||||||
|
<span class="text-sm text-gray-500">Team Rating</span>
|
||||||
|
<span class="text-4xl font-black text-yrtv-600 tracking-tight" x-text="analysisResult.avg_stats.rating.toFixed(2)"></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-2 text-center text-sm">
|
|
||||||
<div class="bg-gray-50 dark:bg-slate-700 p-2 rounded">
|
<div class="grid grid-cols-3 gap-6 text-center">
|
||||||
<div class="text-gray-500">Avg K/D</div>
|
<div class="bg-gray-50 dark:bg-slate-700 p-4 rounded-xl border border-gray-100 dark:border-slate-600">
|
||||||
<div class="font-bold dark:text-white" x-text="analysisResult.avg_stats.kd.toFixed(2)"></div>
|
<div class="text-gray-500 text-xs uppercase tracking-wider mb-1">Avg K/D</div>
|
||||||
|
<div class="text-2xl font-bold dark:text-white" x-text="analysisResult.avg_stats.kd.toFixed(2)"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-50 dark:bg-slate-700 p-2 rounded">
|
<div class="bg-gray-50 dark:bg-slate-700 p-4 rounded-xl border border-gray-100 dark:border-slate-600">
|
||||||
<div class="text-gray-500">Avg ADR</div>
|
<div class="text-gray-500 text-xs uppercase tracking-wider mb-1">Avg ADR</div>
|
||||||
<div class="font-bold dark:text-white" x-text="analysisResult.avg_stats.adr.toFixed(1)"></div>
|
<div class="text-2xl font-bold dark:text-white" x-text="analysisResult.avg_stats.adr.toFixed(1)"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-50 dark:bg-slate-700 p-2 rounded">
|
<div class="bg-gray-50 dark:bg-slate-700 p-4 rounded-xl border border-gray-100 dark:border-slate-600">
|
||||||
<div class="text-gray-500">共同场次</div>
|
<div class="text-gray-500 text-xs uppercase tracking-wider mb-1">Shared Matches</div>
|
||||||
<div class="font-bold dark:text-white" x-text="analysisResult.shared_matches.length"></div>
|
<div class="text-2xl font-bold dark:text-white" x-text="analysisResult.total_shared_matches"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h5 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-2">近期共同比赛 (Recent Shared Matches)</h5>
|
<h5 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-3 flex items-center gap-2">
|
||||||
<div class="max-h-40 overflow-y-auto text-xs">
|
<span>🗓️</span> 共同比赛记录 (Shared Matches History)
|
||||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-slate-600">
|
</h5>
|
||||||
<thead class="bg-gray-50 dark:bg-slate-700">
|
<div class="max-h-60 overflow-y-auto custom-scroll border border-gray-200 dark:border-slate-700 rounded-lg mb-6">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-slate-700">
|
||||||
|
<thead class="bg-gray-50 dark:bg-slate-800 sticky top-0">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-2 py-1 text-left">Map</th>
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Map</th>
|
||||||
<th class="px-2 py-1 text-right">Score</th>
|
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Score</th>
|
||||||
<th class="px-2 py-1 text-right">Result</th>
|
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Result</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 dark:divide-slate-700">
|
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-slate-700">
|
||||||
<template x-for="m in analysisResult.shared_matches" :key="m.match_id">
|
<template x-for="m in analysisResult.shared_matches" :key="m.match_id">
|
||||||
<tr>
|
<tr class="hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors">
|
||||||
<td class="px-2 py-1 dark:text-gray-300" x-text="m.map_name"></td>
|
<td class="px-4 py-3 text-sm font-medium dark:text-gray-300" x-text="m.map_name"></td>
|
||||||
<td class="px-2 py-1 text-right dark:text-gray-300" x-text="m.score_team1 + ':' + m.score_team2"></td>
|
<td class="px-4 py-3 text-sm text-right dark:text-gray-400 font-mono" x-text="m.score_team1 + ':' + m.score_team2"></td>
|
||||||
<td class="px-2 py-1 text-right font-bold" :class="m.is_win ? 'text-green-600' : 'text-red-500'" x-text="m.result_str"></td>
|
<td class="px-4 py-3 text-sm text-right font-bold">
|
||||||
|
<span :class="m.is_win ? 'bg-green-100 text-green-800 px-2 py-0.5 rounded dark:bg-green-900 dark:text-green-200' : 'bg-red-100 text-red-800 px-2 py-0.5 rounded dark:bg-red-900 dark:text-red-200'"
|
||||||
|
x-text="m.result_str"></span>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<template x-if="analysisResult.shared_matches.length === 0">
|
<template x-if="analysisResult.shared_matches.length === 0">
|
||||||
<p class="text-center text-gray-400 py-2">无共同比赛记录</p>
|
<div class="p-8 text-center text-gray-400 bg-gray-50 dark:bg-slate-800">
|
||||||
|
无共同比赛记录
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Map Stats -->
|
||||||
|
<h5 class="text-sm font-bold text-gray-700 dark:text-gray-300 mb-3 flex items-center gap-2">
|
||||||
|
<span>🗺️</span> 地图表现统计 (Map Performance)
|
||||||
|
</h5>
|
||||||
|
<div class="border border-gray-200 dark:border-slate-700 rounded-lg overflow-hidden">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-slate-700">
|
||||||
|
<thead class="bg-gray-50 dark:bg-slate-800">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Map</th>
|
||||||
|
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Matches</th>
|
||||||
|
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Wins</th>
|
||||||
|
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Win Rate</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-slate-700">
|
||||||
|
<template x-for="stat in analysisResult.map_stats" :key="stat.map_name">
|
||||||
|
<tr class="hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors">
|
||||||
|
<td class="px-4 py-2 text-sm font-medium dark:text-gray-300" x-text="stat.map_name"></td>
|
||||||
|
<td class="px-4 py-2 text-sm text-right dark:text-gray-400" x-text="stat.count"></td>
|
||||||
|
<td class="px-4 py-2 text-sm text-right text-green-600 font-bold" x-text="stat.wins"></td>
|
||||||
|
<td class="px-4 py-2 text-sm text-right font-bold dark:text-white">
|
||||||
|
<div class="flex items-center justify-end gap-2">
|
||||||
|
<span x-text="stat.win_rate.toFixed(1) + '%'"></span>
|
||||||
|
<div class="w-16 h-1.5 bg-gray-200 dark:bg-slate-600 rounded-full overflow-hidden">
|
||||||
|
<div class="h-full bg-yrtv-500 rounded-full" :style="'width: ' + stat.win_rate + '%'"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<template x-if="!analysisResult.map_stats || analysisResult.map_stats.length === 0">
|
||||||
|
<div class="p-4 text-center text-gray-400 bg-gray-50 dark:bg-slate-800 text-sm">
|
||||||
|
暂无地图数据
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,6 +342,7 @@ function tacticsApp() {
|
|||||||
// Analysis State
|
// Analysis State
|
||||||
analysisLineup: [],
|
analysisLineup: [],
|
||||||
analysisResult: null,
|
analysisResult: null,
|
||||||
|
debounceTimer: null,
|
||||||
|
|
||||||
// Board State
|
// Board State
|
||||||
currentMap: 'de_mirage',
|
currentMap: 'de_mirage',
|
||||||
@@ -305,6 +359,19 @@ function tacticsApp() {
|
|||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.fetchRoster();
|
this.fetchRoster();
|
||||||
|
|
||||||
|
// Auto-analyze when lineup changes
|
||||||
|
this.$watch('analysisLineup', () => {
|
||||||
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
||||||
|
this.debounceTimer = setTimeout(() => {
|
||||||
|
if (this.analysisLineup.length > 0) {
|
||||||
|
this.analyzeLineup();
|
||||||
|
} else {
|
||||||
|
this.analysisResult = null;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
// Init map on first board view, or delay
|
// Init map on first board view, or delay
|
||||||
this.$watch('activeTab', value => {
|
this.$watch('activeTab', value => {
|
||||||
if (value === 'board') {
|
if (value === 'board') {
|
||||||
|
|||||||
Reference in New Issue
Block a user