Files
yrtv/web/templates/players/profile.html
2026-01-28 01:38:45 +08:00

1217 lines
80 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block content %}
<div class="space-y-8" x-data="{ range: '20' }">
<!-- 1. Header & Data Dashboard (Top) -->
<div class="bg-white dark:bg-slate-800 shadow-xl rounded-2xl overflow-hidden border border-gray-100 dark:border-slate-700">
<div class="p-8">
<div class="lg:flex lg:items-start lg:space-x-8">
<!-- Avatar & Basic Info -->
<div class="flex-shrink-0 flex flex-col items-center lg:items-start space-y-4">
<div class="relative group">
{% if player.avatar_url %}
<img src="{{ player.avatar_url }}" class="h-32 w-32 rounded-2xl object-cover shadow-lg border-4 border-white dark:border-slate-700 transform group-hover:scale-105 transition duration-300">
{% else %}
<div class="h-32 w-32 rounded-2xl bg-gradient-to-br from-yrtv-100 to-yrtv-200 flex items-center justify-center text-yrtv-600 font-bold text-4xl shadow-lg border-4 border-white dark:border-slate-700">
{{ player.username[:2] | upper if player.username else '??' }}
</div>
{% endif %}
{% if session.get('is_admin') %}
<button onclick="document.getElementById('editProfileModal').classList.remove('hidden')" class="absolute -bottom-2 -right-2 bg-white dark:bg-slate-700 p-2 rounded-full shadow-md text-gray-500 hover:text-yrtv-600 transition">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path></svg>
</button>
{% endif %}
</div>
<div class="text-center lg:text-left">
<h1 class="text-3xl font-black text-gray-900 dark:text-white tracking-tight">{{ player.username }}</h1>
<p class="text-sm font-mono text-gray-500 dark:text-gray-400 mt-1">{{ player.steam_id_64 }}</p>
<!-- Tags -->
<div class="mt-3 flex flex-wrap justify-center lg:justify-start gap-2">
{% for tag in metadata.tags %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-md text-xs font-bold bg-gray-100 text-gray-700 dark:bg-slate-700 dark:text-gray-300 border border-gray-200 dark:border-slate-600">
{{ tag }}
{% if session.get('is_admin') %}
<form action="{{ url_for('players.detail', steam_id=player.steam_id_64) }}" method="POST" class="inline ml-1">
<input type="hidden" name="admin_action" value="remove_tag">
<input type="hidden" name="tag" value="{{ tag }}">
<button type="submit" class="text-gray-400 hover:text-red-500 focus:outline-none">&times;</button>
</form>
{% endif %}
</span>
{% endfor %}
{% if session.get('is_admin') %}
<form action="{{ url_for('players.detail', steam_id=player.steam_id_64) }}" method="POST" class="inline-flex">
<input type="hidden" name="admin_action" value="add_tag">
<input type="text" name="tag" placeholder="+Tag" class="w-16 text-xs border border-gray-300 rounded px-1 py-0.5 focus:outline-none dark:bg-slate-700 dark:border-slate-600 dark:text-white">
</form>
{% endif %}
</div>
</div>
</div>
<!-- Data Dashboard -->
<div class="flex-1 w-full mt-8 lg:mt-0">
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
{% macro stat_card(label, metric_key, format_str, icon) %}
{% set dist = distribution[metric_key] if distribution else None %}
<div class="bg-gray-50 dark:bg-slate-700/50 rounded-xl p-5 border border-gray-100 dark:border-slate-600 relative overflow-hidden group hover:shadow-md transition-shadow">
<div class="flex justify-between items-start mb-2">
<div class="text-xs font-bold text-gray-400 uppercase tracking-wider flex items-center gap-1">
{{ icon }} {{ label }}
</div>
{% if dist %}
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold
{% if dist.rank == 1 %}bg-yellow-100 text-yellow-800 border border-yellow-200
{% elif dist.rank <= 3 %}bg-gray-100 text-gray-800 border border-gray-200
{% else %}bg-slate-100 text-slate-600 border border-slate-200{% endif %}">
Rank #{{ dist.rank }}
</span>
{% endif %}
</div>
<div class="text-3xl font-black text-gray-900 dark:text-white mb-3">
{{ format_str.format(dist.val if dist else 0) }}
</div>
<!-- Distribution Bar -->
{% if dist %}
<div class="w-full h-1.5 bg-gray-200 dark:bg-slate-600 rounded-full overflow-hidden relative">
<!-- Range: Min to Max -->
{% set range = dist.max - dist.min %}
{% set percent = ((dist.val - dist.min) / range * 100) if range > 0 else 100 %}
<div class="absolute h-full bg-yrtv-500 rounded-full transition-all duration-1000" style="width: {{ percent }}%"></div>
</div>
<div class="flex justify-between text-[10px] text-gray-400 mt-1 font-mono">
<span>{{ format_str.format(dist.min) }}</span>
<span>Avg: {{ format_str.format(dist.avg) }}</span>
<span>{{ format_str.format(dist.max) }}</span>
</div>
{% else %}
<div class="text-xs text-gray-400">No team data</div>
{% endif %}
</div>
{% endmacro %}
{{ stat_card('Rating', 'rating', '{:.2f}', '⭐') }}
{{ stat_card('K/D Ratio', 'kd', '{:.2f}', '🔫') }}
{{ stat_card('ADR', 'adr', '{:.1f}', '🔥') }}
{{ stat_card('KAST', 'kast', '{:.1%}', '🛡️') }} <!-- Note: KAST is stored as 0-1, formatted as % -->
</div>
</div>
</div>
</div>
</div>
<!-- 2. Charts Section (Middle) -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Trend Chart -->
<div class="lg:col-span-2 bg-white dark:bg-slate-800 shadow-lg rounded-2xl p-6 border border-gray-100 dark:border-slate-700">
<div class="flex justify-between items-center mb-6">
<h3 class="text-lg font-bold text-gray-900 dark:text-white flex items-center gap-2">
<span>📈</span> 近期表现走势 (Performance Trend)
</h3>
<!-- Simple Range Filter (Visual Only for now, could be wired to JS) -->
<div class="flex bg-gray-100 dark:bg-slate-700 rounded-lg p-1">
<button class="px-3 py-1 text-xs font-bold rounded-md bg-white dark:bg-slate-600 shadow-sm text-gray-800 dark:text-white">Recent 20</button>
<!-- <button class="px-3 py-1 text-xs font-medium rounded-md text-gray-500 hover:text-gray-900">All Time</button> -->
</div>
</div>
<div class="relative h-80 w-full">
<canvas id="trendChart"></canvas>
</div>
<div class="mt-4 flex justify-center gap-6 text-xs text-gray-500">
<div class="flex items-center gap-1"><span class="w-3 h-3 rounded-full bg-green-500/20 border border-green-500"></span> Carry (>1.5)</div>
<div class="flex items-center gap-1"><span class="w-3 h-3 rounded-full bg-yellow-500/20 border border-yellow-500"></span> Normal (1.0-1.5)</div>
<div class="flex items-center gap-1"><span class="w-3 h-3 rounded-full bg-red-500/20 border border-red-500"></span> Poor (<0.6)</div>
</div>
</div>
<!-- Radar Chart -->
<div class="bg-white dark:bg-slate-800 shadow-lg rounded-2xl p-6 border border-gray-100 dark:border-slate-700 flex flex-col">
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-2">
<span>🕸️</span> 能力六维图 (Capabilities)
</h3>
<div class="relative flex-1 min-h-[300px] flex items-center justify-center">
<canvas id="radarChart"></canvas>
</div>
</div>
</div>
<!-- 2.5 Detailed Stats Panel -->
<div class="bg-white dark:bg-slate-800 shadow-lg rounded-2xl p-6 border border-gray-100 dark:border-slate-700">
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-2">
<span>📊</span> 详细数据面板 (Detailed Stats)
</h3>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-y-6 gap-x-4">
{% macro detail_item(label, value, key, format_str='{:.2f}', sublabel=None, count_label=None) %}
{% set dist = distribution[key] if distribution else None %}
<div class="flex flex-col group relative h-full">
<div class="flex justify-between items-center mb-1">
<span class="text-xs font-bold text-gray-400 uppercase tracking-wider truncate" title="{{ label }}">{{ label }}</span>
{% if dist %}
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-bold
{% if dist.rank == 1 %}bg-yellow-50 text-yellow-700 border border-yellow-100
{% elif dist.rank <= 3 %}bg-gray-50 text-gray-600 border border-gray-100
{% else %}text-gray-300{% endif %}">
#{{ dist.rank }}
</span>
{% endif %}
</div>
<div class="flex justify-between items-end mb-1">
<div class="flex items-baseline gap-1">
<span class="text-xl font-black text-gray-900 dark:text-white font-mono">
{{ format_str.format(value if value is not none else 0) }}
</span>
{% if sublabel %}
<span class="text-[10px] text-gray-400">{{ sublabel }}</span>
{% endif %}
</div>
{% if count_label is not none %}
<div class="text-[10px] font-bold text-gray-400 font-mono mb-0.5">
{{ count_label }}
</div>
{% endif %}
</div>
<!-- Distribution Bar -->
{% if dist %}
<div class="w-full h-1 bg-gray-100 dark:bg-slate-700 rounded-full overflow-hidden relative mt-1">
{% set range = dist.max - dist.min %}
{% set raw_percent = ((dist.val - dist.min) / range * 100) if range > 0 else 100 %}
{% set percent = (100 - raw_percent) if dist.inverted else raw_percent %}
<div class="absolute h-full bg-yrtv-400/60 rounded-full" style="width: {{ percent }}%"></div>
<!-- Avg Marker -->
{% set raw_avg = ((dist.avg - dist.min) / range * 100) if range > 0 else 50 %}
{% set avg_pct = (100 - raw_avg) if dist.inverted else raw_avg %}
<div class="absolute h-full w-0.5 bg-gray-400 dark:bg-slate-400 top-0" style="left: {{ avg_pct }}%"></div>
</div>
<div class="flex justify-between text-[9px] text-gray-300 dark:text-gray-600 font-mono mt-0.5">
{% if dist.inverted %}
<span>L:{{ format_str.format(dist.max) }}</span>
<span>H:{{ format_str.format(dist.min) }}</span>
{% else %}
<span>L:{{ format_str.format(dist.min) }}</span>
<span>H:{{ format_str.format(dist.max) }}</span>
{% endif %}
</div>
{% endif %}
</div>
{% endmacro %}
<!-- Row 1: Core -->
{{ detail_item('Rating (评分)', features['basic_avg_rating'], 'basic_avg_rating') }}
{{ detail_item('KD Ratio (击杀比)', features['basic_avg_kd'], 'basic_avg_kd') }}
{{ detail_item('KAST (贡献率)', features['basic_avg_kast'], 'basic_avg_kast', '{:.1%}') }}
{{ detail_item('RWS (每局得分)', features['basic_avg_rws'], 'basic_avg_rws') }}
{{ detail_item('ADR (场均伤害)', features['basic_avg_adr'], 'basic_avg_adr', '{:.1f}') }}
<!-- Row 2: Combat -->
{{ detail_item('Avg HS (场均爆头)', features['basic_avg_headshot_kills'], 'basic_avg_headshot_kills') }}
{{ detail_item('HS Rate (爆头率)', features['basic_headshot_rate'], 'basic_headshot_rate', '{:.1%}') }}
{{ detail_item('Assists (场均助攻)', features['basic_avg_assisted_kill'], 'basic_avg_assisted_kill') }}
{{ detail_item('AWP Kills (狙击击杀)', features['basic_avg_awp_kill'], 'basic_avg_awp_kill') }}
{{ detail_item('Jumps (场均跳跃)', features['basic_avg_jump_count'], 'basic_avg_jump_count', '{:.1f}') }}
{{ detail_item('Knife Kills (场均刀杀)', features['basic_avg_knife_kill'], 'basic_avg_knife_kill') }}
{{ detail_item('Zeus Kills (电击枪杀)', features['basic_avg_zeus_kill'], 'basic_avg_zeus_kill') }}
{{ detail_item('Zeus Buy% (起电击枪)', features['basic_zeus_pick_rate'], 'basic_zeus_pick_rate', '{:.1%}') }}
<!-- Row 3: Objective -->
{{ detail_item('MVP (最有价值)', features['basic_avg_mvps'], 'basic_avg_mvps') }}
{{ detail_item('Plants (下包)', features['basic_avg_plants'], 'basic_avg_plants') }}
{{ detail_item('Defuses (拆包)', features['basic_avg_defuses'], 'basic_avg_defuses') }}
{{ detail_item('Flash Assist (闪光助攻)', features['basic_avg_flash_assists'], 'basic_avg_flash_assists') }}
<!-- Row 4: Opening -->
{{ detail_item('First Kill (场均首杀)', features['basic_avg_first_kill'], 'basic_avg_first_kill') }}
{{ detail_item('First Death (场均首死)', features['basic_avg_first_death'], 'basic_avg_first_death') }}
{{ detail_item('FK Rate (首杀率)', features['basic_first_kill_rate'], 'basic_first_kill_rate', '{:.1%}') }}
{{ detail_item('FD Rate (首死率)', features['basic_first_death_rate'], 'basic_first_death_rate', '{:.1%}') }}
<!-- Row 5: Multi-Kills -->
{{ detail_item('2K Rounds (双杀)', features['basic_avg_kill_2'], 'basic_avg_kill_2') }}
{{ detail_item('3K Rounds (三杀)', features['basic_avg_kill_3'], 'basic_avg_kill_3') }}
{{ detail_item('4K Rounds (四杀)', features['basic_avg_kill_4'], 'basic_avg_kill_4') }}
{{ detail_item('5K Rounds (五杀)', features['basic_avg_kill_5'], 'basic_avg_kill_5') }}
<!-- Row 6: Special -->
{{ detail_item('Perfect Kills (无伤杀)', features['basic_avg_perfect_kill'], 'basic_avg_perfect_kill') }}
{{ detail_item('Revenge Kills (复仇杀)', features['basic_avg_revenge_kill'], 'basic_avg_revenge_kill') }}
</div>
</div>
<!-- 2.6 Advanced Dimensions Breakdown -->
<div class="bg-white dark:bg-slate-800 shadow-lg rounded-2xl p-6 border border-gray-100 dark:border-slate-700">
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-2">
<span>🔬</span> 深层能力维度 (Deep Capabilities Breakdown)
</h3>
<!-- Reusing detail_item macro, but with a different grid if needed -->
<!-- Grouped by Dimensions -->
<div class="space-y-8">
<!-- Group 1: STA & BAT -->
<div>
<h4 class="text-xs font-black text-gray-400 uppercase tracking-widest mb-4 border-b border-gray-100 dark:border-slate-700 pb-2">
STA (Stability) & BAT (Aim/Battle)
</h4>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-y-6 gap-x-4">
{{ detail_item('Last 30 Rating (近30场)', features['sta_last_30_rating'], 'sta_last_30_rating') }}
{{ detail_item('Win Rating (胜局)', features['sta_win_rating'], 'sta_win_rating') }}
{{ detail_item('Loss Rating (败局)', features['sta_loss_rating'], 'sta_loss_rating') }}
{{ detail_item('Volatility (波动)', features['sta_rating_volatility'], 'sta_rating_volatility') }}
{{ detail_item('Time Corr (耐力)', features['sta_time_rating_corr'], 'sta_time_rating_corr') }}
{{ detail_item('High Elo KD Diff (高分抗压)', features['bat_kd_diff_high_elo'], 'bat_kd_diff_high_elo') }}
{{ detail_item('Duel Win% (对枪胜率)', features['bat_avg_duel_win_rate'], 'bat_avg_duel_win_rate', '{:.1%}') }}
</div>
</div>
<!-- Group 2: HPS & PTL -->
<div>
<h4 class="text-xs font-black text-gray-400 uppercase tracking-widest mb-4 border-b border-gray-100 dark:border-slate-700 pb-2">
HPS (Clutch/Pressure) & PTL (Pistol)
</h4>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-y-6 gap-x-4">
{{ detail_item('Avg 1v1 (场均1v1)', features['hps_clutch_win_rate_1v1'], 'hps_clutch_win_rate_1v1', '{:.2f}') }}
{{ detail_item('Avg 1v3+ (场均1v3+)', features['hps_clutch_win_rate_1v3_plus'], 'hps_clutch_win_rate_1v3_plus', '{:.2f}') }}
{{ detail_item('Match Pt Win% (赛点胜率)', features['hps_match_point_win_rate'], 'hps_match_point_win_rate', '{:.1%}') }}
{{ detail_item('Pressure Entry (逆风首杀)', features['hps_pressure_entry_rate'], 'hps_pressure_entry_rate', '{:.1%}') }}
{{ detail_item('Comeback KD (翻盘KD)', features['hps_comeback_kd_diff'], 'hps_comeback_kd_diff') }}
{{ detail_item('Loss Streak KD (连败KD)', features['hps_losing_streak_kd_diff'], 'hps_losing_streak_kd_diff') }}
{{ detail_item('Pistol Kills (手枪击杀)', features['ptl_pistol_kills'], 'ptl_pistol_kills') }}
{{ detail_item('Pistol Win% (手枪胜率)', features['ptl_pistol_win_rate'], 'ptl_pistol_win_rate', '{:.1%}') }}
{{ detail_item('Pistol KD (手枪KD)', features['ptl_pistol_kd'], 'ptl_pistol_kd') }}
{{ detail_item('Pistol Util Eff (手枪道具)', features['ptl_pistol_util_efficiency'], 'ptl_pistol_util_efficiency', '{:.1%}') }}
</div>
</div>
<!-- Group 3: UTIL (Utility) -->
<div>
<h4 class="text-xs font-black text-gray-400 uppercase tracking-widest mb-4 border-b border-gray-100 dark:border-slate-700 pb-2">
UTIL (Utility Usage)
</h4>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-y-6 gap-x-4">
{{ detail_item('Usage Rate (道具频率)', features['util_usage_rate'], 'util_usage_rate') }}
{{ detail_item('Nade Dmg (雷火伤)', features['util_avg_nade_dmg'], 'util_avg_nade_dmg', '{:.1f}') }}
{{ detail_item('Flash Time (致盲时间)', features['util_avg_flash_time'], 'util_avg_flash_time', '{:.2f}s') }}
{{ detail_item('Flash Enemy (致盲人数)', features['util_avg_flash_enemy'], 'util_avg_flash_enemy') }}
</div>
</div>
<!-- Group 4: ECO & PACE (New) -->
<div>
<h4 class="text-xs font-black text-gray-400 uppercase tracking-widest mb-4 border-b border-gray-100 dark:border-slate-700 pb-2">
ECO (Economy) & PACE (Tempo)
</h4>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-y-6 gap-x-4">
{{ detail_item('Dmg/$1k (性价比)', features['eco_avg_damage_per_1k'], 'eco_avg_damage_per_1k', '{:.1f}') }}
{{ detail_item('Eco KPR (经济局KPR)', features['eco_rating_eco_rounds'], 'eco_rating_eco_rounds') }}
{{ detail_item('Eco KD (经济局KD)', features['eco_kd_ratio'], 'eco_kd_ratio', '{:.2f}') }}
{{ detail_item('Eco Rounds (经济局数)', features['eco_avg_rounds'], 'eco_avg_rounds', '{:.1f}') }}
{{ detail_item('First Contact (首肯时间)', features['pace_avg_time_to_first_contact'], 'pace_avg_time_to_first_contact', '{:.1f}s') }}
{{ detail_item('Trade Kill% (补枪率)', features['pace_trade_kill_rate'], 'pace_trade_kill_rate', '{:.1%}') }}
{{ detail_item('Opening Time (首杀时间)', features['pace_opening_kill_time'], 'pace_opening_kill_time', '{:.1f}s') }}
{{ detail_item('Avg Life (存活时间)', features['pace_avg_life_time'], 'pace_avg_life_time', '{:.1f}s') }}
</div>
</div>
<div>
<h4 class="text-xs font-black text-gray-400 uppercase tracking-widest mb-4 border-b border-gray-100 dark:border-slate-700 pb-2">
ROUND (Round Dynamics)
</h4>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-y-6 gap-x-4">
{{ detail_item('Kill Early (前30秒击杀)', features['rd_phase_kill_early_share'], 'rd_phase_kill_early_share', '{:.1%}') }}
{{ detail_item('Kill Mid (30-60秒击杀)', features['rd_phase_kill_mid_share'], 'rd_phase_kill_mid_share', '{:.1%}') }}
{{ detail_item('Kill Late (60秒后击杀)', features['rd_phase_kill_late_share'], 'rd_phase_kill_late_share', '{:.1%}') }}
{{ detail_item('Death Early (前30秒死亡)', features['rd_phase_death_early_share'], 'rd_phase_death_early_share', '{:.1%}') }}
{{ detail_item('Death Mid (30-60秒死亡)', features['rd_phase_death_mid_share'], 'rd_phase_death_mid_share', '{:.1%}') }}
{{ detail_item('Death Late (60秒后死亡)', features['rd_phase_death_late_share'], 'rd_phase_death_late_share', '{:.1%}') }}
{{ detail_item('FirstDeath Win% (首死后胜率)', features['rd_firstdeath_team_first_death_win_rate'], 'rd_firstdeath_team_first_death_win_rate', '{:.1%}', count_label=features['rd_firstdeath_team_first_death_rounds']) }}
{{ detail_item('Invalid Death% (无效死亡)', features['rd_invalid_death_rate'], 'rd_invalid_death_rate', '{:.1%}', count_label=features['rd_invalid_death_rounds']) }}
{{ detail_item('Pressure KPR (落后≥3)', features['rd_pressure_kpr_ratio'], 'rd_pressure_kpr_ratio', '{:.2f}x') }}
{{ detail_item('MatchPt KPR (赛点放大)', features['rd_matchpoint_kpr_ratio'], 'rd_matchpoint_kpr_ratio', '{:.2f}x', count_label=features['rd_matchpoint_rounds']) }}
{{ detail_item('Trade Resp (10s响应)', features['rd_trade_response_10s_rate'], 'rd_trade_response_10s_rate', '{:.1%}') }}
{{ detail_item('Pressure Perf (Leetify)', features['rd_pressure_perf_ratio'], 'rd_pressure_perf_ratio', '{:.2f}x') }}
{{ detail_item('MatchPt Perf (Leetify)', features['rd_matchpoint_perf_ratio'], 'rd_matchpoint_perf_ratio', '{:.2f}x') }}
{{ detail_item('Comeback KillShare (追分)', features['rd_comeback_kill_share'], 'rd_comeback_kill_share', '{:.1%}', count_label=features['rd_comeback_rounds']) }}
{{ detail_item('Map Stability (地图稳定)', features['map_stability_coef'], 'map_stability_coef', '{:.3f}') }}
</div>
<div class="mt-6 grid grid-cols-1 lg:grid-cols-3 gap-4">
<div class="bg-gray-50 dark:bg-slate-700/30 rounded-xl p-4 border border-gray-100 dark:border-slate-600">
<div class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Phase Split</div>
{% macro phase_row(title, ke, km, kl, de, dm, dl, ke_key, km_key, kl_key, de_key, dm_key, dl_key) %}
{% set ke = ke or 0 %}
{% set km = km or 0 %}
{% set kl = kl or 0 %}
{% set de = de or 0 %}
{% set dm = dm or 0 %}
{% set dl = dl or 0 %}
{% set k_total = ke + km + kl %}
{% set d_total = de + dm + dl %}
<div class="grid grid-cols-12 gap-2 items-center py-1">
<div class="col-span-2 text-[10px] font-bold text-gray-500 dark:text-gray-400">{{ title }}</div>
<div class="col-span-5">
<div class="w-full h-2 rounded-full overflow-hidden bg-gray-200/60 dark:bg-slate-600/50 flex">
{% if k_total > 0 %}
<div class="h-full bg-yrtv-500" style="width: {{ (ke / k_total) * 100 }}%"></div>
<div class="h-full bg-yrtv-500/70" style="width: {{ (km / k_total) * 100 }}%"></div>
<div class="h-full bg-yrtv-500/40" style="width: {{ (kl / k_total) * 100 }}%"></div>
{% else %}
<div class="h-full w-full bg-gray-300/50 dark:bg-slate-600/40"></div>
{% endif %}
</div>
<div class="mt-0.5 text-[9px] text-gray-400 font-mono flex justify-between">
<span>
E {{ '{:.0%}'.format(ke) }}
{% if distribution and distribution.get(ke_key) %} (#{{ distribution.get(ke_key).rank }}/{{ distribution.get(ke_key).total }}){% endif %}
</span>
<span>
M {{ '{:.0%}'.format(km) }}
{% if distribution and distribution.get(km_key) %} (#{{ distribution.get(km_key).rank }}/{{ distribution.get(km_key).total }}){% endif %}
</span>
<span>
L {{ '{:.0%}'.format(kl) }}
{% if distribution and distribution.get(kl_key) %} (#{{ distribution.get(kl_key).rank }}/{{ distribution.get(kl_key).total }}){% endif %}
</span>
</div>
</div>
<div class="col-span-5">
<div class="w-full h-2 rounded-full overflow-hidden bg-gray-200/60 dark:bg-slate-600/50 flex">
{% if d_total > 0 %}
<div class="h-full bg-slate-400" style="width: {{ (de / d_total) * 100 }}%"></div>
<div class="h-full bg-slate-400/70" style="width: {{ (dm / d_total) * 100 }}%"></div>
<div class="h-full bg-slate-400/40" style="width: {{ (dl / d_total) * 100 }}%"></div>
{% else %}
<div class="h-full w-full bg-gray-300/50 dark:bg-slate-600/40"></div>
{% endif %}
</div>
<div class="mt-0.5 text-[9px] text-gray-400 font-mono flex justify-between">
<span>
E {{ '{:.0%}'.format(de) }}
{% if distribution and distribution.get(de_key) %} (#{{ distribution.get(de_key).rank }}/{{ distribution.get(de_key).total }}){% endif %}
</span>
<span>
M {{ '{:.0%}'.format(dm) }}
{% if distribution and distribution.get(dm_key) %} (#{{ distribution.get(dm_key).rank }}/{{ distribution.get(dm_key).total }}){% endif %}
</span>
<span>
L {{ '{:.0%}'.format(dl) }}
{% if distribution and distribution.get(dl_key) %} (#{{ distribution.get(dl_key).rank }}/{{ distribution.get(dl_key).total }}){% endif %}
</span>
</div>
</div>
</div>
{% endmacro %}
<div class="text-[10px] text-gray-500 dark:text-gray-400 mb-2 grid grid-cols-12 gap-2">
<div class="col-span-2"></div>
<div class="col-span-5 flex justify-between">
<span>Kills</span><span class="font-mono">E / M / L</span>
</div>
<div class="col-span-5 flex justify-between">
<span>Deaths</span><span class="font-mono">E / M / L</span>
</div>
</div>
<div class="space-y-1">
{{ phase_row('Total',
features.get('rd_phase_kill_early_share', 0), features.get('rd_phase_kill_mid_share', 0), features.get('rd_phase_kill_late_share', 0),
features.get('rd_phase_death_early_share', 0), features.get('rd_phase_death_mid_share', 0), features.get('rd_phase_death_late_share', 0),
'rd_phase_kill_early_share', 'rd_phase_kill_mid_share', 'rd_phase_kill_late_share',
'rd_phase_death_early_share', 'rd_phase_death_mid_share', 'rd_phase_death_late_share'
) }}
{{ phase_row('T',
features.get('rd_phase_kill_early_share_t', 0), features.get('rd_phase_kill_mid_share_t', 0), features.get('rd_phase_kill_late_share_t', 0),
features.get('rd_phase_death_early_share_t', 0), features.get('rd_phase_death_mid_share_t', 0), features.get('rd_phase_death_late_share_t', 0),
'rd_phase_kill_early_share_t', 'rd_phase_kill_mid_share_t', 'rd_phase_kill_late_share_t',
'rd_phase_death_early_share_t', 'rd_phase_death_mid_share_t', 'rd_phase_death_late_share_t'
) }}
{{ phase_row('CT',
features.get('rd_phase_kill_early_share_ct', 0), features.get('rd_phase_kill_mid_share_ct', 0), features.get('rd_phase_kill_late_share_ct', 0),
features.get('rd_phase_death_early_share_ct', 0), features.get('rd_phase_death_mid_share_ct', 0), features.get('rd_phase_death_late_share_ct', 0),
'rd_phase_kill_early_share_ct', 'rd_phase_kill_mid_share_ct', 'rd_phase_kill_late_share_ct',
'rd_phase_death_early_share_ct', 'rd_phase_death_mid_share_ct', 'rd_phase_death_late_share_ct'
) }}
</div>
</div>
<div class="bg-gray-50 dark:bg-slate-700/30 rounded-xl p-4 border border-gray-100 dark:border-slate-600">
<div class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Top Weapons</div>
<div id="weaponTopTable" class="text-sm"></div>
</div>
<div class="bg-gray-50 dark:bg-slate-700/30 rounded-xl p-4 border border-gray-100 dark:border-slate-600">
<div class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Round Type Split</div>
<div class="text-[11px] text-gray-500 dark:text-gray-400 mb-2">
KPR=Kills per Round每回合击杀 · Perf=Leetify Round Performance Score回合表现分
</div>
<div id="roundTypeTable" class="text-sm"></div>
</div>
</div>
</div>
<!-- Group 5: SPECIAL (Clutch & Multi) -->
<div>
<h4 class="text-xs font-black text-gray-400 uppercase tracking-widest mb-4 border-b border-gray-100 dark:border-slate-700 pb-2">
SPECIAL (Clutch & Multi)
</h4>
{% set matches = l2_stats.get('matches', 0) or 1 %}
{% set rounds = l2_stats.get('total_rounds', 0) or 1 %}
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-y-6 gap-x-4">
{% set c1 = l2_stats.get('c1', 0) or 0 %}
{% set a1 = l2_stats.get('att1', 0) or 0 %}
{{ detail_item('1v1 Win% (1v1胜率)', c1 / a1 if a1 > 0 else 0, 'clutch_rate_1v1', '{:.1%}', count_label=c1 ~ '/' ~ a1) }}
{% set c2 = l2_stats.get('c2', 0) or 0 %}
{% set a2 = l2_stats.get('att2', 0) or 0 %}
{{ detail_item('1v2 Win% (1v2胜率)', c2 / a2 if a2 > 0 else 0, 'clutch_rate_1v2', '{:.1%}', count_label=c2 ~ '/' ~ a2) }}
{% set c3 = l2_stats.get('c3', 0) or 0 %}
{% set a3 = l2_stats.get('att3', 0) or 0 %}
{{ detail_item('1v3 Win% (1v3胜率)', c3 / a3 if a3 > 0 else 0, 'clutch_rate_1v3', '{:.1%}', count_label=c3 ~ '/' ~ a3) }}
{% set c4 = l2_stats.get('c4', 0) or 0 %}
{% set a4 = l2_stats.get('att4', 0) or 0 %}
{{ detail_item('1v4 Win% (1v4胜率)', c4 / a4 if a4 > 0 else 0, 'clutch_rate_1v4', '{:.1%}', count_label=c4 ~ '/' ~ a4) }}
{% set c5 = l2_stats.get('c5', 0) or 0 %}
{% set a5 = l2_stats.get('att5', 0) or 0 %}
{{ detail_item('1v5 Win% (1v5胜率)', c5 / a5 if a5 > 0 else 0, 'clutch_rate_1v5', '{:.1%}', count_label=c5 ~ '/' ~ a5) }}
{% set mk_count = (l2_stats.get('k2', 0) or 0) + (l2_stats.get('k3', 0) or 0) + (l2_stats.get('k4', 0) or 0) + (l2_stats.get('k5', 0) or 0) %}
{% set ma_count = (l2_stats.get('a2', 0) or 0) + (l2_stats.get('a3', 0) or 0) + (l2_stats.get('a4', 0) or 0) + (l2_stats.get('a5', 0) or 0) %}
{{ detail_item('Multi-K Rate (多杀率)', mk_count / rounds, 'total_multikill_rate', '{:.1%}', count_label=mk_count) }}
{{ detail_item('Multi-A Rate (多助率)', ma_count / rounds, 'total_multiassist_rate', '{:.1%}', count_label=ma_count) }}
</div>
</div>
<!-- Group 4: SIDE (T/CT Preference) -->
<div>
<h4 class="text-xs font-black text-gray-400 uppercase tracking-widest mb-4 border-b border-gray-100 dark:border-slate-700 pb-2">
SIDE (T/CT Preference)
</h4>
{% macro vs_item_val(label, t_val, ct_val, format_str='{:.2f}') %}
{% set diff = ct_val - t_val %}
{# Dynamic Sizing #}
{% set t_size = 'text-2xl' if t_val > ct_val else 'text-sm text-gray-500 dark:text-gray-400' %}
{% set ct_size = 'text-2xl' if ct_val > t_val else 'text-sm text-gray-500 dark:text-gray-400' %}
{% if t_val == ct_val %}
{% set t_size = 'text-lg' %}
{% set ct_size = 'text-lg' %}
{% endif %}
<div class="bg-gray-50 dark:bg-slate-700/30 rounded-xl p-4 border border-gray-100 dark:border-slate-600 relative overflow-hidden group hover:shadow-md transition-all">
<!-- Header with Diff -->
<div class="flex justify-between items-start mb-3">
<span class="text-xs font-bold text-gray-400 uppercase tracking-wider">{{ label }}</span>
{% if diff|abs > 0.001 %}
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-black tracking-wide
{% if diff > 0 %}bg-blue-100 text-blue-700 border border-blue-200
{% else %}bg-amber-100 text-amber-700 border border-amber-200{% endif %}">
{% if diff > 0 %}CT +{{ format_str.format(diff) }}
{% else %}T +{{ format_str.format(diff|abs) }}{% endif %}
</span>
{% endif %}
</div>
<!-- Values -->
<div class="flex items-end justify-between gap-2">
<!-- T Side -->
<div class="flex flex-col items-start">
<span class="text-xs font-bold text-amber-600/80 dark:text-amber-500 mb-0.5">T-Side</span>
<span class="{{ t_size }} font-black font-mono leading-none transition-all">
{{ format_str.format(t_val) }}
</span>
</div>
<!-- VS Divider -->
<div class="h-8 w-px bg-gray-200 dark:bg-slate-600 mx-1"></div>
<!-- CT Side -->
<div class="flex flex-col items-end">
<span class="text-xs font-bold text-blue-600/80 dark:text-blue-400 mb-0.5">CT-Side</span>
<span class="{{ ct_size }} font-black font-mono leading-none transition-all">
{{ format_str.format(ct_val) }}
</span>
</div>
</div>
<!-- Mini Bar for visual comparison -->
<div class="mt-3 flex h-1.5 w-full rounded-full overflow-hidden bg-gray-200 dark:bg-slate-600">
{% set total = t_val + ct_val %}
{% if total > 0 %}
{% set t_pct = (t_val / total) * 100 %}
<div class="h-full bg-amber-500" style="width: {{ t_pct }}%"></div>
<div class="h-full bg-blue-500 flex-1"></div>
{% else %}
<div class="h-full w-1/2 bg-gray-300"></div>
<div class="h-full w-1/2 bg-gray-400"></div>
{% endif %}
</div>
</div>
{% endmacro %}
{% macro vs_item(label, t_key, ct_key, format_str='{:.2f}') %}
{{ vs_item_val(label, features[t_key] or 0, features[ct_key] or 0, format_str) }}
{% endmacro %}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{{ vs_item('Rating (Rating/KD)', 'side_rating_t', 'side_rating_ct') }}
{{ vs_item('KD Ratio', 'side_kd_t', 'side_kd_ct') }}
{{ vs_item('Win Rate (胜率)', 'side_win_rate_t', 'side_win_rate_ct', '{:.1%}') }}
{{ vs_item('First Kill Rate (首杀率)', 'side_first_kill_rate_t', 'side_first_kill_rate_ct', '{:.1%}') }}
{{ vs_item('First Death Rate (首死率)', 'side_first_death_rate_t', 'side_first_death_rate_ct', '{:.1%}') }}
{{ vs_item('KAST (贡献率)', 'side_kast_t', 'side_kast_ct', '{:.1%}') }}
{{ vs_item('RWS (Round Win Share)', 'side_rws_t', 'side_rws_ct') }}
{{ vs_item('Headshot Rate (爆头率)', 'side_headshot_rate_t', 'side_headshot_rate_ct', '{:.1%}') }}
{# New Comparisons #}
{% set t_rounds = side_stats.get('T', {}).get('rounds', 0) or 1 %}
{% set ct_rounds = side_stats.get('CT', {}).get('rounds', 0) or 1 %}
{% set t_clutch = (side_stats.get('T', {}).get('total_clutch', 0) or 0) / t_rounds %}
{% set ct_clutch = (side_stats.get('CT', {}).get('total_clutch', 0) or 0) / ct_rounds %}
{{ vs_item_val('Clutch Win Rate (残局率)', t_clutch, ct_clutch, '{:.1%}') }}
{% set t_mk = (side_stats.get('T', {}).get('total_multikill', 0) or 0) / t_rounds %}
{% set ct_mk = (side_stats.get('CT', {}).get('total_multikill', 0) or 0) / ct_rounds %}
{{ vs_item_val('Multi-Kill Rate (多杀率)', t_mk, ct_mk, '{:.1%}') }}
{% set t_ma = (side_stats.get('T', {}).get('total_multiassist', 0) or 0) / t_rounds %}
{% set ct_ma = (side_stats.get('CT', {}).get('total_multiassist', 0) or 0) / ct_rounds %}
{{ vs_item_val('Multi-Assist Rate (多助攻)', t_ma, ct_ma, '{:.1%}') }}
</div>
</div>
<!-- New Section: Party & Stratification -->
<div>
<h4 class="text-xs font-black text-gray-400 uppercase tracking-widest mb-4 border-b border-gray-100 dark:border-slate-700 pb-2">
👥 组排与分层表现 (Party & Stratification)
</h4>
<div class="space-y-8">
<!-- Group 1: Party Size -->
<div>
<h5 class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-3">Party Size Performance (组排表现)</h5>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-y-6 gap-x-4">
{{ detail_item('Solo Win% (单排胜率)', features['party_1_win_rate'], 'party_1_win_rate', '{:.1%}') }}
{{ detail_item('Solo Rating (单排分)', features['party_1_rating'], 'party_1_rating') }}
{{ detail_item('Solo ADR (单排伤)', features['party_1_adr'], 'party_1_adr', '{:.1f}') }}
{{ detail_item('Duo Win% (双排胜率)', features['party_2_win_rate'], 'party_2_win_rate', '{:.1%}') }}
{{ detail_item('Duo Rating (双排分)', features['party_2_rating'], 'party_2_rating') }}
{{ detail_item('Duo ADR (双排伤)', features['party_2_adr'], 'party_2_adr', '{:.1f}') }}
{{ detail_item('Trio Win% (三排胜率)', features['party_3_win_rate'], 'party_3_win_rate', '{:.1%}') }}
{{ detail_item('Trio Rating (三排分)', features['party_3_rating'], 'party_3_rating') }}
{{ detail_item('Trio ADR (三排伤)', features['party_3_adr'], 'party_3_adr', '{:.1f}') }}
{{ detail_item('Quad Win% (四排胜率)', features['party_4_win_rate'], 'party_4_win_rate', '{:.1%}') }}
{{ detail_item('Quad Rating (四排分)', features['party_4_rating'], 'party_4_rating') }}
{{ detail_item('Quad ADR (四排伤)', features['party_4_adr'], 'party_4_adr', '{:.1f}') }}
{{ detail_item('Full Win% (五排胜率)', features['party_5_win_rate'], 'party_5_win_rate', '{:.1%}') }}
{{ detail_item('Full Rating (五排分)', features['party_5_rating'], 'party_5_rating') }}
{{ detail_item('Full ADR (五排伤)', features['party_5_adr'], 'party_5_adr', '{:.1f}') }}
</div>
</div>
<!-- Group 2: Rating Distribution -->
<div>
<h5 class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-3">Performance Tiers (表现分层)</h5>
<div class="grid grid-cols-2 md:grid-cols-4 gap-y-6 gap-x-4">
{{ detail_item('Carry Rate (>1.5)', features['rating_dist_carry_rate'], 'rating_dist_carry_rate', '{:.1%}') }}
{{ detail_item('Normal Rate (1.0-1.5)', features['rating_dist_normal_rate'], 'rating_dist_normal_rate', '{:.1%}') }}
{{ detail_item('Sacrifice Rate (0.6-1.0)', features['rating_dist_sacrifice_rate'], 'rating_dist_sacrifice_rate', '{:.1%}') }}
{{ detail_item('Sleeping Rate (<0.6)', features['rating_dist_sleeping_rate'], 'rating_dist_sleeping_rate', '{:.1%}') }}
</div>
</div>
<!-- Group 3: ELO Stratification -->
<div>
<h5 class="text-[10px] font-bold text-gray-400 uppercase tracking-wider mb-3">Performance vs ELO (不同分段表现)</h5>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-y-6 gap-x-4">
{{ detail_item('<1200 Rating', features['elo_lt1200_rating'], 'elo_lt1200_rating') }}
{{ detail_item('1200-1400 Rating', features['elo_1200_1400_rating'], 'elo_1200_1400_rating') }}
{{ detail_item('1400-1600 Rating', features['elo_1400_1600_rating'], 'elo_1400_1600_rating') }}
{{ detail_item('1600-1800 Rating', features['elo_1600_1800_rating'], 'elo_1600_1800_rating') }}
{{ detail_item('1800-2000 Rating', features['elo_1800_2000_rating'], 'elo_1800_2000_rating') }}
{{ detail_item('>2000 Rating', features['elo_gt2000_rating'], 'elo_gt2000_rating') }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 3. Match History & Comments (Bottom) -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Match History Table -->
<div class="lg:col-span-2 bg-white dark:bg-slate-800 shadow-lg rounded-2xl overflow-hidden border border-gray-100 dark:border-slate-700">
<div class="p-6 border-b border-gray-100 dark:border-slate-700 flex justify-between items-center">
<h3 class="text-lg font-bold text-gray-900 dark:text-white">比赛记录 (Match History)</h3>
<span class="px-2.5 py-0.5 rounded-full text-xs font-bold bg-gray-100 text-gray-600 dark:bg-slate-700 dark:text-gray-300">
{{ history|length }} Matches
</span>
</div>
<div class="overflow-x-auto max-h-[600px] overflow-y-auto custom-scroll">
<table class="min-w-full divide-y divide-gray-200 dark:divide-slate-700">
<thead class="bg-gray-50 dark:bg-slate-700/50 sticky top-0 backdrop-blur-sm z-10">
<tr>
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">Date/Map</th>
<th class="px-6 py-3 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">Result</th>
<th class="px-6 py-3 text-right text-xs font-bold text-gray-500 uppercase tracking-wider">Rating</th>
<th class="px-6 py-3 text-right text-xs font-bold text-gray-500 uppercase tracking-wider">K/D</th>
<th class="px-6 py-3 text-right text-xs font-bold text-gray-500 uppercase tracking-wider">ADR</th>
<th class="px-6 py-3 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">Link</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-slate-700 bg-white dark:bg-slate-800">
{% for m in history | reverse %}
<tr class="hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors group">
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-bold text-gray-900 dark:text-white">{{ m.map_name }}</div>
<div class="text-xs text-gray-500 font-mono">
<script>document.write(new Date({{ m.start_time }} * 1000).toLocaleDateString())</script>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-center">
<div class="flex flex-col items-center gap-1">
<span class="px-2.5 py-0.5 rounded text-[10px] font-black uppercase tracking-wide
{% if m.is_win %}bg-green-100 text-green-700 border border-green-200
{% else %}bg-red-50 text-red-600 border border-red-100{% endif %}">
{{ 'WIN' if m.is_win else 'LOSS' }}
</span>
{% if m.party_size and m.party_size > 1 %}
<span class="text-[10px] text-gray-400 flex items-center gap-0.5" title="Party Size">
👥 {{ m.party_size }}
</span>
{% endif %}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right">
{% set r = m.rating or 0 %}
<div class="flex items-center justify-end gap-2">
<span class="text-sm font-bold font-mono {% if r >= 1.5 %}text-yrtv-600{% elif r >= 1.1 %}text-green-600{% elif r < 0.6 %}text-red-500{% else %}text-gray-700 dark:text-gray-300{% endif %}">
{{ "%.2f"|format(r) }}
</span>
<!-- Mini Bar -->
<div class="w-12 h-1 bg-gray-100 dark:bg-slate-700 rounded-full overflow-hidden">
<div class="h-full {% if r >= 1.1 %}bg-green-500{% elif r < 0.9 %}bg-red-500{% else %}bg-gray-400{% endif %}" style="width: {{ (r / 2.0 * 100)|int }}%"></div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm text-gray-600 dark:text-gray-400 font-mono">
{{ "%.2f"|format(m.kd_ratio or 0) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm text-gray-600 dark:text-gray-400 font-mono">
{{ "%.1f"|format(m.adr or 0) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-center">
<a href="{{ url_for('matches.detail', match_id=m.match_id) }}" class="p-2 text-gray-400 hover:text-yrtv-600 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="6" class="px-6 py-12 text-center text-gray-400">
<div class="text-4xl mb-2">🏜️</div>
No matches recorded yet.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Right Column: Map Stats & Comments -->
<div class="space-y-8">
<!-- Map Stats -->
<div class="bg-white dark:bg-slate-800 shadow-lg rounded-2xl p-6 border border-gray-100 dark:border-slate-700">
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-4">地图数据 (Map Stats)</h3>
<div class="space-y-3 max-h-[400px] overflow-y-auto custom-scroll pr-1">
{% for m in map_stats %}
<div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-slate-700/30 rounded-xl hover:bg-gray-100 transition-colors">
<div class="flex items-center gap-3">
<!-- Map Icon/Name -->
<div class="w-10 h-10 rounded-lg bg-gray-200 dark:bg-slate-600 flex items-center justify-center text-xs font-black text-gray-500 uppercase">
{{ m.map_name[:3] }}
</div>
<div>
<div class="text-sm font-bold text-gray-900 dark:text-white">{{ m.map_name }}</div>
<div class="text-xs text-gray-500 font-mono">{{ m.matches }} matches</div>
</div>
</div>
<div class="text-right">
<div class="text-sm font-black font-mono {% if m.rating >= 1.1 %}text-green-600{% elif m.rating < 0.9 %}text-red-500{% else %}text-gray-700 dark:text-gray-300{% endif %}">
{{ "%.2f"|format(m.rating) }}
</div>
<div class="flex items-center justify-end gap-2 text-[10px] text-gray-400 font-mono">
<span class="{% if m.win_rate >= 0.5 %}text-green-600{% else %}text-red-500{% endif %}">{{ "%.0f"|format(m.win_rate * 100) }}% Win</span>
<span>{{ "%.1f"|format(m.adr) }} ADR</span>
</div>
</div>
</div>
{% else %}
<div class="text-center py-4 text-gray-400 text-sm">No map data available.</div>
{% endfor %}
</div>
</div>
<!-- Reviews / Comments -->
<div class="bg-white dark:bg-slate-800 shadow-lg rounded-2xl p-6 border border-gray-100 dark:border-slate-700">
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-6">留言板 (Comments)</h3>
<form action="{{ url_for('players.detail', steam_id=player.steam_id_64) }}" method="POST" class="mb-8 relative">
<input type="text" name="username" class="absolute top-2 left-2 text-xs border-none bg-transparent focus:ring-0 text-gray-500 w-full" placeholder="Name (Optional)">
<textarea name="content" rows="3" required class="block w-full pt-8 pb-2 px-3 border border-gray-200 dark:border-slate-600 rounded-xl bg-gray-50 dark:bg-slate-700/50 focus:ring-2 focus:ring-yrtv-500 focus:bg-white dark:focus:bg-slate-700 transition" placeholder="Write a comment..."></textarea>
<button type="submit" class="absolute bottom-2 right-2 px-3 py-1 bg-yrtv-600 text-white text-xs font-bold rounded-lg hover:bg-yrtv-700 transition shadow-sm">Post</button>
</form>
<div class="space-y-4 max-h-[500px] overflow-y-auto custom-scroll pr-2">
{% for comment in comments %}
<div class="flex gap-3 group">
<div class="flex-shrink-0 mt-1">
<div class="h-8 w-8 rounded-full bg-gradient-to-br from-gray-100 to-gray-200 flex items-center justify-center text-gray-500 text-xs font-bold border border-white shadow-sm">
{{ comment.username[:1] | upper }}
</div>
</div>
<div class="flex-1 bg-gray-50 dark:bg-slate-700/30 rounded-r-xl rounded-bl-xl p-3 text-sm hover:bg-gray-100 dark:hover:bg-slate-700/50 transition-colors">
<div class="flex justify-between items-baseline mb-1">
<span class="font-bold text-gray-900 dark:text-white">{{ comment.username }}</span>
<span class="text-xs text-gray-400">{{ comment.created_at }}</span>
</div>
<p class="text-gray-600 dark:text-gray-300 leading-relaxed">{{ comment.content }}</p>
<div class="mt-2 flex justify-end">
<button onclick="likeComment({{ comment.id }}, this)" class="text-xs text-gray-400 hover:text-red-500 flex items-center gap-1 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/></svg>
<span class="like-count font-bold">{{ comment.likes }}</span>
</button>
</div>
</div>
</div>
{% else %}
<div class="text-center py-8 text-gray-400 text-sm">No comments yet.</div>
{% endfor %}
</div>
</div>
</div>
</div>
<!-- Edit Modal (Hidden) -->
<div id="editProfileModal" class="hidden fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center">
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl w-full max-w-md p-6 m-4 animate-scale-in">
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Edit Profile</h3>
<form action="{{ url_for('players.detail', steam_id=player.steam_id_64) }}" method="POST" enctype="multipart/form-data">
<input type="hidden" name="admin_action" value="update_profile">
<div class="space-y-4">
<div>
<label class="block text-sm font-bold text-gray-700 dark:text-gray-300 mb-1">Avatar</label>
<input type="file" name="avatar" accept="image/*" class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-bold file:bg-yrtv-50 file:text-yrtv-700 hover:file:bg-yrtv-100">
</div>
<div>
<label class="block text-sm font-bold text-gray-700 dark:text-gray-300 mb-1">Notes</label>
<textarea name="notes" rows="3" class="w-full border-gray-300 rounded-lg shadow-sm focus:border-yrtv-500 focus:ring-yrtv-500 dark:bg-slate-700 dark:border-slate-600 dark:text-white">{{ metadata.notes }}</textarea>
</div>
</div>
<div class="mt-6 flex gap-3">
<button type="button" onclick="document.getElementById('editProfileModal').classList.add('hidden')" class="flex-1 px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 font-bold transition">Cancel</button>
<button type="submit" class="flex-1 px-4 py-2 bg-yrtv-600 text-white rounded-lg hover:bg-yrtv-700 font-bold shadow-lg shadow-yrtv-500/30 transition">Save Changes</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let trendChartInstance = null;
function resetZoom() {
if (trendChartInstance) {
trendChartInstance.resetZoom();
}
}
function likeComment(commentId, btn) {
fetch(`/players/comment/${commentId}/like`, { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.success) {
const countSpan = btn.querySelector('.like-count');
countSpan.innerText = parseInt(countSpan.innerText) + 1;
btn.classList.add('text-red-500');
}
});
}
document.addEventListener('DOMContentLoaded', function() {
const steamId = "{{ player.steam_id_64 }}";
fetch(`/players/${steamId}/charts_data`)
.then(response => response.json())
.then(data => {
// Register Zoom Plugin Manually if needed (usually auto-registers in UMD)
if (window.ChartZoom) {
Chart.register(window.ChartZoom);
}
// Radar Chart
const ctxRadar = document.getElementById('radarChart').getContext('2d');
// Prepare Distribution Data
const dist = data.radar_dist || {};
const getDist = (key) => dist[key] || { rank: '?', avg: 0 };
// Map friendly names to keys
const keys = ['score_bat', 'score_hps', 'score_ptl', 'score_tct', 'score_util', 'score_sta', 'score_eco', 'score_pace'];
// Corresponding Labels
const rawLabels = ['Aim (BAT)', 'Clutch (HPS)', 'Pistol (PTL)', 'Defense (SIDE)', 'Util (UTIL)', 'Stability (STA)', 'Economy (ECO)', 'Pace (PACE)'];
const labels = rawLabels.map((l, i) => {
const k = keys[i];
const d = getDist(k);
return `${l} #${d.rank}`;
});
const teamAvgs = keys.map(k => getDist(k).avg);
new Chart(ctxRadar, {
type: 'radar',
data: {
// Update labels to friendly names
labels: labels,
datasets: [{
label: 'Player',
data: [
data.radar.BAT, data.radar.HPS,
data.radar.PTL, data.radar.SIDE, data.radar.UTIL,
data.radar.STA, data.radar.ECO, data.radar.PACE
],
backgroundColor: 'rgba(124, 58, 237, 0.2)',
borderColor: '#7c3aed',
borderWidth: 2,
pointBackgroundColor: '#7c3aed',
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: '#7c3aed'
},
{
label: 'Team Avg',
data: teamAvgs,
backgroundColor: 'rgba(148, 163, 184, 0.2)', // Slate-400
borderColor: '#94a3b8',
borderWidth: 2,
pointRadius: 0,
borderDash: [5, 5]
}]
},
options: {
plugins: {
legend: { display: true, position: 'bottom' }
},
scales: {
r: {
beginAtZero: true,
suggestedMax: 100,
angleLines: {
color: 'rgba(156, 163, 175, 0.2)'
},
grid: {
color: 'rgba(156, 163, 175, 0.2)'
},
pointLabels: {
font: {
size: 11,
weight: 'bold'
},
color: '#6b7280' // gray-500
},
ticks: {
display: false // Hide numbers on axis
}
}
}
}
});
// Trend Chart
const ctxTrend = document.getElementById('trendChart').getContext('2d');
// Create Gradient
const gradient = ctxTrend.createLinearGradient(0, 0, 0, 400);
gradient.addColorStop(0, 'rgba(124, 58, 237, 0.5)'); // Purple
gradient.addColorStop(1, 'rgba(124, 58, 237, 0.0)');
trendChartInstance = new Chart(ctxTrend, {
type: 'line',
data: {
labels: data.trend.labels,
datasets: [
{
label: 'Rating',
data: data.trend.values,
borderColor: '#7c3aed', // YRTV Purple
backgroundColor: gradient,
borderWidth: 2,
tension: 0.4, // Smoother curve
pointRadius: 3,
pointBackgroundColor: '#fff',
pointBorderColor: '#7c3aed',
pointHoverRadius: 6,
pointHoverBackgroundColor: '#7c3aed',
pointHoverBorderColor: '#fff',
fill: true,
order: 1
},
// Baselines
{
label: 'Carry (1.5)',
data: Array(data.trend.labels.length).fill(1.5),
borderColor: 'rgba(34, 197, 94, 0.6)', // Green
borderWidth: 1,
borderDash: [5, 5],
pointRadius: 0,
fill: false,
order: 2
},
{
label: 'Normal (1.0)',
data: Array(data.trend.labels.length).fill(1.0),
borderColor: 'rgba(234, 179, 8, 0.6)', // Yellow
borderWidth: 1,
borderDash: [5, 5],
pointRadius: 0,
fill: false,
order: 3
},
{
label: 'Poor (0.6)',
data: Array(data.trend.labels.length).fill(0.6),
borderColor: 'rgba(239, 68, 68, 0.6)', // Red
borderWidth: 1,
borderDash: [5, 5],
pointRadius: 0,
fill: false,
order: 4
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false,
},
plugins: {
legend: {
display: false
},
zoom: {
pan: {
enabled: true,
mode: 'x',
modifierKey: null, // Allow plain drag
},
zoom: {
wheel: {
enabled: true,
},
pinch: {
enabled: true
},
mode: 'x',
}
},
tooltip: {
backgroundColor: 'rgba(17, 24, 39, 0.9)',
titleFont: { size: 12 },
bodyFont: { size: 14, weight: 'bold' },
padding: 12,
cornerRadius: 8,
displayColors: false, // Cleaner look
callbacks: {
label: function(context) {
if (context.datasetIndex > 0) return null; // Hide baseline tooltips
let val = context.parsed.y.toFixed(2);
let label = "Rating: " + val;
if (val >= 1.5) label += " 🔥";
else if (val < 0.6) label += " 💀";
return label;
}
}
}
},
scales: {
y: {
beginAtZero: true,
suggestedMax: 2.0,
grid: {
color: 'rgba(156, 163, 175, 0.1)',
borderDash: [2, 2]
},
ticks: {
font: { size: 10 }
}
},
x: {
grid: {
display: false
},
ticks: {
maxRotation: 0, // Keep labels horizontal
minRotation: 0,
autoSkip: true,
maxTicksLimit: 10, // Avoid crowding
font: { size: 10 }
}
}
}
}
});
const phaseCanvas = document.getElementById('phaseChart');
if (phaseCanvas) {
phaseCanvas.remove();
}
const weaponTop = JSON.parse({{ (features.get('rd_weapon_top_json', '[]') or '[]') | tojson }});
const weaponTopEl = document.getElementById('weaponTopTable');
if (weaponTopEl) {
if (!Array.isArray(weaponTop) || weaponTop.length === 0) {
weaponTopEl.innerHTML = '<div class="text-gray-500 dark:text-gray-400">No data</div>';
} else {
const matchesPlayed = Number({{ features.get('total_matches', 0) or 0 }}) || 0;
const weaponRankMap = {{ (distribution.get('top_weapon_rank_map', {}) or {}) | tojson }};
const rows = weaponTop.map(w => {
const kills = Number(w.kills || 0);
const hsRate = Number(w.hs_rate || 0);
const kpm = matchesPlayed > 0 ? (kills / matchesPlayed) : kills;
return { ...w, kills, hsRate, kpm };
});
rows.sort((a, b) => b.kpm - a.kpm);
const catMap = { pistol: '副武器', smg: '冲锋枪', shotgun: '霰弹枪', rifle: '步枪', sniper: '狙击枪', lmg: '重机枪' };
const fmtPct = (v) => `${(v * 100).toFixed(1)}%`;
weaponTopEl.innerHTML = `
<div class="overflow-x-auto">
<table class="w-full text-xs">
<thead class="text-gray-500 dark:text-gray-400">
<tr>
<th class="text-left font-bold py-1 pr-2">武器</th>
<th class="text-right font-bold py-1 px-2">击杀</th>
<th class="text-right font-bold py-1 px-2">爆头率</th>
<th class="text-left font-bold py-1 pl-2">价格/类型</th>
</tr>
</thead>
<tbody class="text-gray-700 dark:text-gray-200">
${rows.map((w) => {
const category = catMap[w.category] || (w.category || '');
const price = (w.price != null) ? `$${w.price}` : '—';
const info = weaponRankMap[w.weapon] || {};
const kpmRank = (info.kpm_rank != null && info.kpm_total != null) ? `#${info.kpm_rank}/${info.kpm_total}` : '—';
const hsRank = (info.hs_rank != null && info.hs_total != null) ? `#${info.hs_rank}/${info.hs_total}` : '—';
const killCell = `${w.kills} (场均 ${w.kpm.toFixed(2)} · ${kpmRank})`;
const hsCell = `${fmtPct(w.hsRate)} (${hsRank})`;
const priceType = `${price}${category ? '-' + category : ''}`;
return `
<tr class="border-t border-gray-100 dark:border-slate-600/40">
<td class="py-1 pr-2 font-mono">${w.weapon}</td>
<td class="py-1 px-2 text-right font-mono">${killCell}</td>
<td class="py-1 px-2 text-right font-mono">${hsCell}</td>
<td class="py-1 pl-2 font-mono">${priceType}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
`;
}
}
const roundSplit = JSON.parse({{ (features.get('rd_roundtype_split_json', '{}') or '{}') | tojson }});
const roundSplitEl = document.getElementById('roundTypeTable');
if (roundSplitEl) {
const keys = Object.keys(roundSplit || {});
if (keys.length === 0) {
roundSplitEl.innerHTML = '<div class="text-gray-500 dark:text-gray-400">No data</div>';
} else {
const order = ['pistol', 'reg', 'eco', 'rifle', 'fullbuy', 'overtime'];
keys.sort((a, b) => order.indexOf(a) - order.indexOf(b));
const rtRank = {
pistol: { kpr: { rank: {{ (distribution.get('rd_rt_kpr_pistol') or {}).get('rank', 'null') }}, total: {{ (distribution.get('rd_rt_kpr_pistol') or {}).get('total', 'null') }} } },
reg: { kpr: { rank: {{ (distribution.get('rd_rt_kpr_reg') or {}).get('rank', 'null') }}, total: {{ (distribution.get('rd_rt_kpr_reg') or {}).get('total', 'null') }} } },
overtime: { kpr: { rank: {{ (distribution.get('rd_rt_kpr_overtime') or {}).get('rank', 'null') }}, total: {{ (distribution.get('rd_rt_kpr_overtime') or {}).get('total', 'null') }} },
perf: { rank: {{ (distribution.get('rd_rt_perf_overtime') or {}).get('rank', 'null') }}, total: {{ (distribution.get('rd_rt_perf_overtime') or {}).get('total', 'null') }} } },
eco: { perf: { rank: {{ (distribution.get('rd_rt_perf_eco') or {}).get('rank', 'null') }}, total: {{ (distribution.get('rd_rt_perf_eco') or {}).get('total', 'null') }} } },
rifle: { perf: { rank: {{ (distribution.get('rd_rt_perf_rifle') or {}).get('rank', 'null') }}, total: {{ (distribution.get('rd_rt_perf_rifle') or {}).get('total', 'null') }} } },
fullbuy: { perf: { rank: {{ (distribution.get('rd_rt_perf_fullbuy') or {}).get('rank', 'null') }}, total: {{ (distribution.get('rd_rt_perf_fullbuy') or {}).get('total', 'null') }} } },
};
const fmtRank = (r) => (r && r.rank != null && r.total != null) ? `#${r.rank}/${r.total}` : '—';
roundSplitEl.innerHTML = `
<div class="overflow-x-auto">
<table class="w-full text-xs">
<thead class="text-gray-500 dark:text-gray-400">
<tr>
<th class="text-left font-bold py-1 pr-2">类型</th>
<th class="text-right font-bold py-1 px-2">KPR</th>
<th class="text-right font-bold py-1 px-2">队内</th>
<th class="text-right font-bold py-1 px-2">Perf</th>
<th class="text-right font-bold py-1 px-2">队内</th>
<th class="text-right font-bold py-1 pl-2">样本</th>
</tr>
</thead>
<tbody class="text-gray-700 dark:text-gray-200">
${keys.map(k => {
const v = roundSplit[k] || {};
const kpr = (v.kpr != null) ? Number(v.kpr).toFixed(2) : '—';
const perf = (v.perf != null) ? Number(v.perf).toFixed(2) : '—';
const rounds = v.rounds != null ? v.rounds : 0;
const rk = rtRank[k] || {};
const kprRank = fmtRank(rk.kpr);
const perfRank = fmtRank(rk.perf);
return `
<tr class="border-t border-gray-100 dark:border-slate-600/40">
<td class="py-1 pr-2 font-mono">${k}</td>
<td class="py-1 px-2 text-right font-mono">${kpr}</td>
<td class="py-1 px-2 text-right font-mono">${kprRank}</td>
<td class="py-1 px-2 text-right font-mono">${perf}</td>
<td class="py-1 px-2 text-right font-mono">${perfRank}</td>
<td class="py-1 pl-2 text-right font-mono">n=${rounds}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
`;
}
}
});
});
</script>
{% endblock %}