1.0.1-fix: Fixed 'winner-team' regarded as win.

This commit is contained in:
2026-01-26 02:22:09 +08:00
parent 8dabf0b097
commit 81739392da
5 changed files with 130 additions and 18 deletions

View File

@@ -44,6 +44,7 @@ def api_analyze():
# 2. Shared Matches
shared_matches = StatsService.get_shared_matches(steam_ids)
# They are already dicts now with 'result_str' and 'is_win'
# 3. Aggregates
avg_stats = {

View File

@@ -178,19 +178,29 @@ class StatsService:
@staticmethod
def get_shared_matches(steam_ids):
# Find matches where ALL steam_ids were present in the SAME team (or just present?)
# "共同经历" usually means played together.
# Query: Intersect match_ids for each player.
# SQLite doesn't have INTERSECT ALL easily for dynamic list, but we can group by match_id.
if not steam_ids or len(steam_ids) < 2:
# Find matches where ALL steam_ids were present
if not steam_ids or len(steam_ids) < 1:
return []
placeholders = ','.join('?' for _ in steam_ids)
count = len(steam_ids)
# We need to know which team the players were on to determine win/loss
# Assuming they were on the SAME team for "shared experience"
# If count=1, it's just match history
# Query: Get matches where all steam_ids are present
# Also join to get team_id to check if they were on the same team (optional but better)
# For simplicity in v1: Just check presence in the match.
# AND check if the player won.
# We need to return: match_id, map_name, score, result (Win/Loss)
# "Result" is relative to the lineup.
# If they were on the winning team, it's a Win.
sql = f"""
SELECT m.match_id, m.start_time, m.map_name, m.score_team1, m.score_team2, m.winner_team
SELECT m.match_id, m.start_time, m.map_name, m.score_team1, m.score_team2, m.winner_team,
MAX(mp.team_id) as player_team_id -- Just take one team_id (assuming same)
FROM fact_matches m
JOIN fact_match_players mp ON m.match_id = mp.match_id
WHERE mp.steam_id_64 IN ({placeholders})
@@ -203,7 +213,33 @@ class StatsService:
args = list(steam_ids)
args.append(count)
return query_db('l2', sql, args)
rows = query_db('l2', sql, args)
results = []
for r in rows:
# Determine if Win
# winner_team in DB is 'Team 1' or 'Team 2' usually, or the team name.
# fact_matches.winner_team stores the NAME of the winner? Or 'team1'/'team2'?
# Let's check how L2_Builder stores it. Usually it stores the name.
# But fact_match_players.team_id stores the name too.
# Logic: If m.winner_team == mp.team_id, then Win.
is_win = (r['winner_team'] == r['player_team_id'])
# If winner_team is NULL or empty, it's a draw?
if not r['winner_team']:
result_str = 'Draw'
elif is_win:
result_str = 'Win'
else:
result_str = 'Loss'
res = dict(r)
res['is_win'] = is_win # Boolean for styling
res['result_str'] = result_str # Text for display
results.append(res)
return results
@staticmethod
def get_player_trend(steam_id, limit=20):

View File

@@ -34,8 +34,14 @@
draggable="true"
@dragstart="dragStart($event, player)">
<img :src="player.avatar_url || 'https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg'"
class="w-10 h-10 rounded-full border border-gray-200 dark:border-slate-600 object-cover pointer-events-none">
<template x-if="player.avatar_url">
<img :src="player.avatar_url" class="w-10 h-10 rounded-full border border-gray-200 dark:border-slate-600 object-cover pointer-events-none">
</template>
<template x-if="!player.avatar_url">
<div class="w-10 h-10 rounded-full bg-yrtv-100 flex items-center justify-center border border-gray-200 dark:border-slate-600 text-yrtv-600 font-bold text-xs pointer-events-none">
<span x-text="(player.username || player.name || player.steam_id_64).substring(0, 2).toUpperCase()"></span>
</div>
</template>
<div class="ml-3 flex-1 min-w-0 pointer-events-none">
<div class="text-sm font-medium text-gray-900 dark:text-white truncate" x-text="player.username || player.name || player.steam_id_64"></div>
@@ -96,9 +102,19 @@
<div class="grid grid-cols-1 sm:grid-cols-5 gap-4">
<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 bg-gray-50 dark:bg-slate-700 p-2 rounded border border-gray-200 dark:border-slate-600 flex flex-col items-center">
<button @click="removeFromAnalysis(idx)" class="absolute top-1 right-1 text-red-400 hover:text-red-600">×</button>
<img :src="p.avatar_url" class="w-12 h-12 rounded-full mb-2">
<!-- Avatar -->
<template x-if="p.avatar_url">
<img :src="p.avatar_url" class="w-12 h-12 rounded-full mb-2 object-cover">
</template>
<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">
<span x-text="(p.username || p.name || p.steam_id_64).substring(0, 2).toUpperCase()"></span>
</div>
</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-[10px] text-gray-500" x-text="'R: ' + (p.stats?.basic_avg_rating || 0).toFixed(2)"></span>
</div>
@@ -163,7 +179,7 @@
<tr>
<td class="px-2 py-1 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-2 py-1 text-right font-bold" :class="m.winner_team ? 'text-green-600' : 'text-gray-500'" x-text="m.winner_team ? 'Win' : 'Draw/Loss'"></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>
</tr>
</template>
</tbody>
@@ -411,8 +427,10 @@ function tacticsApp() {
const displayName = player.username || player.name || player.steam_id_64;
const iconHtml = `
<div class="flex flex-col items-center justify-center transform hover:scale-110 transition duration-200">
<img src="${player.avatar_url || 'https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg'}"
class="w-8 h-8 rounded-full border-2 border-white shadow-lg box-content">
${player.avatar_url ?
`<img src="${player.avatar_url}" class="w-8 h-8 rounded-full border-2 border-white shadow-lg box-content object-cover">` :
`<div class="w-8 h-8 rounded-full bg-yrtv-100 border-2 border-white shadow-lg box-content flex items-center justify-center text-yrtv-600 font-bold text-[10px]">${(player.username || player.name).substring(0, 2).toUpperCase()}</div>`
}
<span class="mt-1 text-[10px] font-bold text-white bg-black/60 px-1.5 py-0.5 rounded backdrop-blur-sm whitespace-nowrap overflow-hidden max-w-[80px] text-ellipsis">
${displayName}
</span>

View File

@@ -21,8 +21,23 @@
</div>
</div>
<!-- Active Roster (Grid) -->
<div class="mb-10">
<!-- Sorting Controls -->
<div class="flex justify-end mb-4">
<div class="inline-flex shadow-sm rounded-md" role="group">
<button type="button" @click="sortBy('rating')" :class="{'bg-yrtv-600 text-white': currentSort === 'rating', 'bg-white text-gray-700 hover:bg-gray-50': currentSort !== 'rating'}" class="px-4 py-2 text-sm font-medium border border-gray-200 rounded-l-lg dark:bg-slate-700 dark:border-slate-600 dark:text-white dark:hover:bg-slate-600">
Rating
</button>
<button type="button" @click="sortBy('kd')" :class="{'bg-yrtv-600 text-white': currentSort === 'kd', 'bg-white text-gray-700 hover:bg-gray-50': currentSort !== 'kd'}" class="px-4 py-2 text-sm font-medium border-t border-b border-gray-200 dark:bg-slate-700 dark:border-slate-600 dark:text-white dark:hover:bg-slate-600">
K/D
</button>
<button type="button" @click="sortBy('matches')" :class="{'bg-yrtv-600 text-white': currentSort === 'matches', 'bg-white text-gray-700 hover:bg-gray-50': currentSort !== 'matches'}" class="px-4 py-2 text-sm font-medium border border-gray-200 rounded-r-lg dark:bg-slate-700 dark:border-slate-600 dark:text-white dark:hover:bg-slate-600">
Matches
</button>
</div>
</div>
<!-- Active Roster (Grid) -->
<div class="mb-10">
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white mb-4">Active Roster</h3>
<!-- Dynamic Grid based on roster size, default to 5 slots + 1 add button -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-6">
@@ -32,7 +47,15 @@
<div class="w-full h-full flex flex-col items-center">
<div class="relative w-32 h-32 mb-4">
<img :src="player.avatar_url || 'https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg'" class="w-32 h-32 rounded-full object-cover border-4 border-yrtv-500 shadow-lg">
<!-- Avatar Logic: Image or Initials -->
<template x-if="player.avatar_url">
<img :src="player.avatar_url" class="w-32 h-32 rounded-full object-cover border-4 border-yrtv-500 shadow-lg">
</template>
<template x-if="!player.avatar_url">
<div class="w-32 h-32 rounded-full bg-yrtv-100 flex items-center justify-center border-4 border-yrtv-500 shadow-lg text-yrtv-600 font-bold text-4xl">
<span x-text="(player.username || player.name || player.steam_id_64).substring(0, 2).toUpperCase()"></span>
</div>
</template>
</div>
<h4 class="text-lg font-bold text-gray-900 dark:text-white truncate w-full text-center" x-text="player.username || player.name || player.steam_id_64"></h4>
@@ -140,6 +163,7 @@ function clubhouse() {
return {
team: {},
roster: [],
currentSort: 'rating', // Default sort
showScoutModal: false,
searchQuery: '',
searchResults: [],
@@ -154,9 +178,42 @@ function clubhouse() {
.then(data => {
this.team = data.team;
this.roster = data.roster;
this.sortRoster(); // Apply default sort
});
},
sortBy(key) {
this.currentSort = key;
this.sortRoster();
},
sortRoster() {
if (!this.roster || this.roster.length === 0) return;
this.roster.sort((a, b) => {
let valA = 0, valB = 0;
if (this.currentSort === 'rating') {
valA = a.stats?.basic_avg_rating || 0;
valB = b.stats?.basic_avg_rating || 0;
} else if (this.currentSort === 'kd') {
valA = a.stats?.basic_avg_kd || 0;
valB = b.stats?.basic_avg_kd || 0;
} else if (this.currentSort === 'matches') {
// matches_played is usually on the player object now? or stats?
// Check API: it's not explicitly in 'stats', but search added it.
// Roster API usually doesn't attach matches_played unless we ask.
// Let's assume stats.total_matches or check object root.
// Looking at roster API: we attach match counts? No, only search.
// But we can use total_matches from stats.
valA = a.stats?.total_matches || 0;
valB = b.stats?.total_matches || 0;
}
return valB - valA; // Descending
});
},
searchPlayers() {
if (this.searchQuery.length < 2) {
this.searchResults = [];