1.0.2-hotfix: Added matchlist new features.

This commit is contained in:
2026-01-26 02:53:31 +08:00
parent 81739392da
commit d8b70c1cf7
7 changed files with 320 additions and 33 deletions

View File

@@ -23,11 +23,66 @@ def detail(match_id):
return "Match not found", 404
players = StatsService.get_match_players(match_id)
# Convert sqlite3.Row objects to dicts to allow modification
players = [dict(p) for p in players]
rounds = StatsService.get_match_rounds(match_id)
# Organize players by team
team1_players = [p for p in players if p['team_id'] == 1]
team2_players = [p for p in players if p['team_id'] == 2]
# --- Roster Identification ---
# Fetch active roster to identify "Our Team" players
from web.services.web_service import WebService
lineups = WebService.get_lineups()
# Assume we use the first/active lineup
active_roster_ids = []
if lineups:
try:
active_roster_ids = json.loads(lineups[0]['player_ids_json'])
except:
pass
# Mark roster players (Ensure strict string comparison)
roster_set = set(str(uid) for uid in active_roster_ids)
for p in players:
p['is_in_roster'] = str(p['steam_id_64']) in roster_set
# --- Party Size Calculation ---
# Only calculate party size for OUR ROSTER members.
# Group roster members by match_team_id
roster_parties = {} # match_team_id -> count of roster members
for p in players:
if p['is_in_roster']:
mtid = p.get('match_team_id')
if mtid and mtid > 0:
key = f"tid_{mtid}"
roster_parties[key] = roster_parties.get(key, 0) + 1
# Assign party size ONLY to roster members
for p in players:
if p['is_in_roster']:
mtid = p.get('match_team_id')
if mtid and mtid > 0:
p['party_size'] = roster_parties.get(f"tid_{mtid}", 1)
else:
p['party_size'] = 1 # Solo roster player
else:
p['party_size'] = 0 # Hide party info for non-roster players
# Organize players by Side (team_id)
# team_id 1 = Team 1, team_id 2 = Team 2
# Note: group_id 1/2 usually corresponds to Team 1/2.
# Fallback to team_id if group_id is missing or 0 (legacy data compatibility)
team1_players = [p for p in players if p.get('group_id') == 1]
team2_players = [p for p in players if p.get('group_id') == 2]
# If group_id didn't work (empty lists), try team_id grouping (if team_id is 1/2 only)
if not team1_players and not team2_players:
team1_players = [p for p in players if p['team_id'] == 1]
team2_players = [p for p in players if p['team_id'] == 2]
# Explicitly sort by Rating DESC
team1_players.sort(key=lambda x: x.get('rating', 0) or 0, reverse=True)
team2_players.sort(key=lambda x: x.get('rating', 0) or 0, reverse=True)
return render_template('matches/detail.html', match=match,
team1_players=team1_players, team2_players=team2_players,

View File

@@ -44,6 +44,109 @@ class StatsService:
matches = query_db('l2', sql, args)
# Enrich matches with Avg ELO, Party info, and Our Team Result
if matches:
match_ids = [m['match_id'] for m in matches]
placeholders = ','.join('?' for _ in match_ids)
# Fetch ELO
elo_sql = f"""
SELECT match_id, AVG(group_origin_elo) as avg_elo
FROM fact_match_teams
WHERE match_id IN ({placeholders}) AND group_origin_elo > 0
GROUP BY match_id
"""
elo_rows = query_db('l2', elo_sql, match_ids)
elo_map = {row['match_id']: row['avg_elo'] for row in elo_rows}
# Fetch Max Party Size
party_sql = f"""
SELECT match_id, MAX(cnt) as max_party
FROM (
SELECT match_id, match_team_id, COUNT(*) as cnt
FROM fact_match_players
WHERE match_id IN ({placeholders}) AND match_team_id > 0
GROUP BY match_id, match_team_id
)
GROUP BY match_id
"""
party_rows = query_db('l2', party_sql, match_ids)
party_map = {row['match_id']: row['max_party'] for row in party_rows}
# --- New: Determine "Our Team" Result ---
# Logic: Check if any player from `active_roster` played in these matches.
# Use WebService to get the active roster
from web.services.web_service import WebService
import json
lineups = WebService.get_lineups()
active_roster_ids = []
if lineups:
try:
# Load IDs and ensure they are all strings for DB comparison consistency
raw_ids = json.loads(lineups[0]['player_ids_json'])
active_roster_ids = [str(uid) for uid in raw_ids]
except:
pass
# If no roster, we can't determine "Our Result"
if not active_roster_ids:
result_map = {}
else:
roster_placeholders = ','.join('?' for _ in active_roster_ids)
# We cast steam_id_64 to TEXT to ensure match even if stored as int
our_result_sql = f"""
SELECT mp.match_id, mp.team_id, m.winner_team, COUNT(*) as our_count
FROM fact_match_players mp
JOIN fact_matches m ON mp.match_id = m.match_id
WHERE mp.match_id IN ({placeholders})
AND CAST(mp.steam_id_64 AS TEXT) IN ({roster_placeholders})
GROUP BY mp.match_id, mp.team_id
"""
# Combine args: match_ids + roster_ids
combined_args = match_ids + active_roster_ids
our_rows = query_db('l2', our_result_sql, combined_args)
# Map match_id -> result ('win', 'loss', 'draw', 'mixed')
result_map = {}
match_sides = {}
match_winners = {}
for r in our_rows:
mid = r['match_id']
if mid not in match_sides: match_sides[mid] = {}
match_sides[mid][r['team_id']] = r['our_count']
match_winners[mid] = r['winner_team']
for mid, sides in match_sides.items():
winner = match_winners.get(mid)
if not winner:
result_map[mid] = 'draw'
continue
our_on_winner = sides.get(winner, 0)
loser = 2 if winner == 1 else 1
our_on_loser = sides.get(loser, 0)
if our_on_winner > 0 and our_on_loser == 0:
result_map[mid] = 'win'
elif our_on_loser > 0 and our_on_winner == 0:
result_map[mid] = 'loss'
elif our_on_winner > 0 and our_on_loser > 0:
result_map[mid] = 'mixed'
else:
result_map[mid] = None
# Convert to dict to modify
matches = [dict(m) for m in matches]
for m in matches:
m['avg_elo'] = elo_map.get(m['match_id'], 0)
m['max_party'] = party_map.get(m['match_id'], 1)
m['our_result'] = result_map.get(m['match_id'])
# Count total for pagination
count_sql = f"SELECT COUNT(*) as cnt FROM fact_matches WHERE {where_str}"
total = query_db('l2', count_sql, args[:-2], one=True)['cnt']
@@ -243,20 +346,25 @@ class StatsService:
@staticmethod
def get_player_trend(steam_id, limit=20):
sql = """
SELECT m.start_time, mp.rating, mp.kd_ratio, mp.adr, m.match_id, m.map_name
FROM fact_match_players mp
JOIN fact_matches m ON mp.match_id = m.match_id
WHERE mp.steam_id_64 = ?
ORDER BY m.start_time ASC
"""
# We fetch all then slice last 'limit' in python or use subquery.
# DESC LIMIT gets recent, but we want chronological for chart.
# So: SELECT ... ORDER BY time DESC LIMIT ? -> then reverse in code.
# We need party_size: count of players with same match_team_id in the same match
# Using a correlated subquery for party_size
sql = """
SELECT * FROM (
SELECT m.start_time, mp.rating, mp.kd_ratio, mp.adr, m.match_id, m.map_name, mp.is_win
SELECT
m.start_time,
mp.rating,
mp.kd_ratio,
mp.adr,
m.match_id,
m.map_name,
mp.is_win,
mp.match_team_id,
(SELECT COUNT(*)
FROM fact_match_players p2
WHERE p2.match_id = mp.match_id
AND p2.match_team_id = mp.match_team_id
AND p2.match_team_id > 0 -- Ensure we don't count 0 (solo default) as a massive party
) as party_size
FROM fact_match_players mp
JOIN fact_matches m ON mp.match_id = m.match_id
WHERE mp.steam_id_64 = ?

View File

@@ -47,12 +47,35 @@
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-8 w-8">
<img class="h-8 w-8 rounded-full" src="{{ p.avatar_url or 'https://via.placeholder.com/32' }}" alt="">
{% if p.avatar_url %}
<img class="h-8 w-8 rounded-full" src="{{ p.avatar_url }}" alt="">
{% else %}
<div class="h-8 w-8 rounded-full bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-xs border border-yrtv-200">
{{ (p.username or p.steam_id_64)[:2] | upper }}
</div>
{% endif %}
</div>
<div class="ml-4">
<a href="{{ url_for('players.detail', steam_id=p.steam_id_64) }}" class="text-sm font-medium text-gray-900 dark:text-white hover:text-yrtv-600">
{{ p.username or p.steam_id_64 }}
</a>
<div class="flex items-center space-x-2">
<a href="{{ url_for('players.detail', steam_id=p.steam_id_64) }}" class="text-sm font-medium text-gray-900 dark:text-white hover:text-yrtv-600">
{{ p.username or p.steam_id_64 }}
</a>
{% if p.party_size > 1 %}
{% set pc = p.party_size %}
{% set p_color = 'bg-blue-100 text-blue-800' %}
{% if pc == 2 %}{% set p_color = 'bg-indigo-100 text-indigo-800' %}
{% elif pc == 3 %}{% set p_color = 'bg-blue-100 text-blue-800' %}
{% elif pc == 4 %}{% set p_color = 'bg-purple-100 text-purple-800' %}
{% elif pc >= 5 %}{% set p_color = 'bg-orange-100 text-orange-800' %}
{% endif %}
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium {{ p_color }} dark:bg-opacity-20" title="Roster Party of {{ p.party_size }}">
<svg class="mr-1 h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
<path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z" />
</svg>
{{ p.party_size }}
</span>
{% endif %}
</div>
</div>
</div>
</td>
@@ -97,12 +120,35 @@
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-8 w-8">
<img class="h-8 w-8 rounded-full" src="{{ p.avatar_url or 'https://via.placeholder.com/32' }}" alt="">
{% if p.avatar_url %}
<img class="h-8 w-8 rounded-full" src="{{ p.avatar_url }}" alt="">
{% else %}
<div class="h-8 w-8 rounded-full bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-xs border border-yrtv-200">
{{ (p.username or p.steam_id_64)[:2] | upper }}
</div>
{% endif %}
</div>
<div class="ml-4">
<a href="{{ url_for('players.detail', steam_id=p.steam_id_64) }}" class="text-sm font-medium text-gray-900 dark:text-white hover:text-yrtv-600">
{{ p.username or p.steam_id_64 }}
</a>
<div class="flex items-center space-x-2">
<a href="{{ url_for('players.detail', steam_id=p.steam_id_64) }}" class="text-sm font-medium text-gray-900 dark:text-white hover:text-yrtv-600">
{{ p.username or p.steam_id_64 }}
</a>
{% if p.party_size > 1 %}
{% set pc = p.party_size %}
{% set p_color = 'bg-blue-100 text-blue-800' %}
{% if pc == 2 %}{% set p_color = 'bg-indigo-100 text-indigo-800' %}
{% elif pc == 3 %}{% set p_color = 'bg-blue-100 text-blue-800' %}
{% elif pc == 4 %}{% set p_color = 'bg-purple-100 text-purple-800' %}
{% elif pc >= 5 %}{% set p_color = 'bg-orange-100 text-orange-800' %}
{% endif %}
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium {{ p_color }} dark:bg-opacity-20" title="Roster Party of {{ p.party_size }}">
<svg class="mr-1 h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
<path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z" />
</svg>
{{ p.party_size }}
</span>
{% endif %}
</div>
</div>
</div>
</td>

View File

@@ -17,6 +17,8 @@
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">时间</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">地图</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">比分</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">ELO</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Party</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">时长</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">操作</th>
</tr>
@@ -31,13 +33,62 @@
{{ match.map_name }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {% if match.winner_team == 1 %}bg-green-100 text-green-800{% else %}bg-red-100 text-red-800{% endif %}">
{{ match.score_team1 }}
</span>
-
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {% if match.winner_team == 2 %}bg-green-100 text-green-800{% else %}bg-red-100 text-red-800{% endif %}">
{{ match.score_team2 }}
</span>
<div class="flex items-center space-x-2">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {% if match.winner_team == 1 %}bg-green-100 text-green-800 border border-green-200{% else %}bg-gray-100 text-gray-500{% endif %}">
{{ match.score_team1 }}
{% if match.winner_team == 1 %}
<svg class="ml-1 h-3 w-3" fill="currentColor" viewBox="0 0 20 20"><path d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" /></svg>
{% endif %}
</span>
<span class="text-gray-400">-</span>
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {% if match.winner_team == 2 %}bg-green-100 text-green-800 border border-green-200{% else %}bg-gray-100 text-gray-500{% endif %}">
{{ match.score_team2 }}
{% if match.winner_team == 2 %}
<svg class="ml-1 h-3 w-3" fill="currentColor" viewBox="0 0 20 20"><path d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" /></svg>
{% endif %}
</span>
<!-- Our Team Result Badge -->
{% if match.our_result %}
{% if match.our_result == 'win' %}
<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-green-500 text-white">
VICTORY
</span>
{% elif match.our_result == 'loss' %}
<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-red-500 text-white">
DEFEAT
</span>
{% elif match.our_result == 'mixed' %}
<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-yellow-500 text-white">
CIVIL WAR
</span>
{% endif %}
{% endif %}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{% if match.avg_elo and match.avg_elo > 0 %}
<span class="font-mono">{{ "%.0f"|format(match.avg_elo) }}</span>
{% else %}
<span class="text-xs text-gray-300">-</span>
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{% if match.max_party and match.max_party > 1 %}
{% set p = match.max_party %}
{% set party_class = 'bg-gray-100 text-gray-800' %}
{% if p == 2 %} {% set party_class = 'bg-indigo-100 text-indigo-800 border border-indigo-200' %}
{% elif p == 3 %} {% set party_class = 'bg-blue-100 text-blue-800 border border-blue-200' %}
{% elif p == 4 %} {% set party_class = 'bg-purple-100 text-purple-800 border border-purple-200' %}
{% elif p >= 5 %} {% set party_class = 'bg-orange-100 text-orange-800 border border-orange-200' %}
{% endif %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {{ party_class }}">
👥 {{ match.max_party }}
</span>
{% else %}
<span class="text-xs text-gray-300">Solo</span>
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ (match.duration / 60) | int }} min

View File

@@ -26,10 +26,14 @@
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{% for player in players %}
<div class="bg-gray-50 dark:bg-slate-700 rounded-lg p-4 flex flex-col items-center hover:shadow-lg transition">
<!-- Avatar Hidden/Placeholder -->
<div class="h-20 w-20 rounded-full mb-4 bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-2xl">
<!-- Avatar -->
{% if player.avatar_url %}
<img class="h-20 w-20 rounded-full mb-4 object-cover border-4 border-white shadow-sm" src="{{ player.avatar_url }}" alt="{{ player.username }}">
{% else %}
<div class="h-20 w-20 rounded-full mb-4 bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-2xl border-4 border-white shadow-sm">
{{ player.username[:2] | upper if player.username else '??' }}
</div>
{% endif %}
<h3 class="text-lg font-medium text-gray-900 dark:text-white">{{ player.username }}</h3>
<p class="text-sm text-gray-500 mb-2">{{ player.steam_id_64 }}</p>

View File

@@ -150,6 +150,8 @@
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Date</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Map</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Result</th>
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Party</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Rating</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">K/D</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">ADR</th>
@@ -157,12 +159,33 @@
</tr>
</thead>
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-gray-700">
{% for m in history %}
{% for m in history | reverse %}
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
<script>document.write(new Date({{ m.start_time }} * 1000).toLocaleDateString())</script>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ m.map_name }}</td>
<td class="px-6 py-4 whitespace-nowrap text-center">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {% if m.is_win %}bg-green-100 text-green-800{% else %}bg-red-100 text-red-800{% endif %}">
{{ 'WIN' if m.is_win else 'LOSS' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-center text-sm text-gray-500 dark:text-gray-400">
{% if m.party_size and m.party_size > 1 %}
{% set p = m.party_size %}
{% set party_class = 'bg-gray-100 text-gray-800' %}
{% if p == 2 %} {% set party_class = 'bg-indigo-100 text-indigo-800' %}
{% elif p == 3 %} {% set party_class = 'bg-blue-100 text-blue-800' %}
{% elif p == 4 %} {% set party_class = 'bg-purple-100 text-purple-800' %}
{% elif p >= 5 %} {% set party_class = 'bg-orange-100 text-orange-800' %}
{% endif %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {{ party_class }}">
👥 {{ m.party_size }}
</span>
{% else %}
<span class="text-xs text-gray-400">Solo</span>
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-right font-bold {% if (m.rating or 0) >= 1.1 %}text-green-600{% elif (m.rating or 0) < 0.9 %}text-red-600{% else %}text-gray-900 dark:text-white{% endif %}">{{ "%.2f"|format(m.rating or 0) }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ "%.2f"|format(m.kd_ratio or 0) }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ "%.1f"|format(m.adr or 0) }}</td>