2026-01-26 02:13:06 +08:00
|
|
|
|
{% extends "base.html" %}
|
|
|
|
|
|
|
|
|
|
|
|
{% block content %}
|
2026-01-26 18:36:47 +08:00
|
|
|
|
<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>
|
2026-01-26 02:13:06 +08:00
|
|
|
|
|
2026-01-26 18:36:47 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
|
2026-01-26 02:13:06 +08:00
|
|
|
|
<!-- Tags -->
|
2026-01-26 18:36:47 +08:00
|
|
|
|
<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">×</button>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{% endfor %}
|
|
|
|
|
|
|
2026-01-26 02:13:06 +08:00
|
|
|
|
{% if session.get('is_admin') %}
|
2026-01-26 18:36:47 +08:00
|
|
|
|
<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">
|
2026-01-26 02:13:06 +08:00
|
|
|
|
</form>
|
|
|
|
|
|
{% endif %}
|
2026-01-26 18:36:47 +08:00
|
|
|
|
</div>
|
2026-01-26 02:13:06 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-26 18:36:47 +08:00
|
|
|
|
<!-- 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 %}
|
2026-01-27 00:57:35 +08:00
|
|
|
|
<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 %}
|
2026-01-26 18:36:47 +08:00
|
|
|
|
</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 %}
|
2026-01-26 02:13:06 +08:00
|
|
|
|
|
2026-01-26 18:36:47 +08:00
|
|
|
|
{{ 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 % -->
|
2026-01-26 02:13:06 +08:00
|
|
|
|
</div>
|
2026-01-26 18:36:47 +08:00
|
|
|
|
</div>
|
2026-01-26 02:13:06 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-26 18:36:47 +08:00
|
|
|
|
<!-- 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>
|
2026-01-26 02:13:06 +08:00
|
|
|
|
</div>
|
2026-01-26 18:36:47 +08:00
|
|
|
|
<div class="relative h-80 w-full">
|
|
|
|
|
|
<canvas id="trendChart"></canvas>
|
2026-01-26 02:13:06 +08:00
|
|
|
|
</div>
|
2026-01-26 18:36:47 +08:00
|
|
|
|
<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>
|
2026-01-26 02:13:06 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-26 18:36:47 +08:00
|
|
|
|
<!-- 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">
|
2026-01-26 02:13:06 +08:00
|
|
|
|
<canvas id="radarChart"></canvas>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-26 18:36:47 +08:00
|
|
|
|
|
2026-01-26 21:10:42 +08:00
|
|
|
|
<!-- 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">
|
2026-01-27 03:11:17 +08:00
|
|
|
|
{% macro detail_item(label, value, key, format_str='{:.2f}', sublabel=None, count_label=None) %}
|
2026-01-26 21:10:42 +08:00
|
|
|
|
{% set dist = distribution[key] if distribution else None %}
|
2026-01-27 03:11:17 +08:00
|
|
|
|
<div class="flex flex-col group relative h-full">
|
2026-01-26 21:10:42 +08:00
|
|
|
|
<div class="flex justify-between items-center mb-1">
|
2026-01-27 03:11:17 +08:00
|
|
|
|
<span class="text-xs font-bold text-gray-400 uppercase tracking-wider truncate" title="{{ label }}">{{ label }}</span>
|
2026-01-26 21:10:42 +08:00
|
|
|
|
{% if dist %}
|
2026-01-27 00:57:35 +08:00
|
|
|
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-bold
|
2026-01-26 21:10:42 +08:00
|
|
|
|
{% 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>
|
|
|
|
|
|
|
2026-01-27 17:53:09 +08:00
|
|
|
|
<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>
|
2026-01-26 21:10:42 +08:00
|
|
|
|
{% 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 %}
|
2026-01-27 21:26:07 +08:00
|
|
|
|
{% 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 %}
|
2026-01-26 21:10:42 +08:00
|
|
|
|
<div class="absolute h-full bg-yrtv-400/60 rounded-full" style="width: {{ percent }}%"></div>
|
|
|
|
|
|
<!-- Avg Marker -->
|
2026-01-27 21:26:07 +08:00
|
|
|
|
{% 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 %}
|
2026-01-26 21:10:42 +08:00
|
|
|
|
<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">
|
2026-01-27 21:26:07 +08:00
|
|
|
|
{% 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 %}
|
2026-01-26 21:10:42 +08:00
|
|
|
|
</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}') }}
|
2026-01-28 01:20:26 +08:00
|
|
|
|
{{ 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%}') }}
|
2026-01-26 21:10:42 +08:00
|
|
|
|
|
2026-01-26 22:04:29 +08:00
|
|
|
|
<!-- 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 -->
|
2026-01-26 21:10:42 +08:00
|
|
|
|
{{ 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%}') }}
|
|
|
|
|
|
|
2026-01-26 22:04:29 +08:00
|
|
|
|
<!-- Row 5: Multi-Kills -->
|
2026-01-26 21:10:42 +08:00
|
|
|
|
{{ 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') }}
|
|
|
|
|
|
|
2026-01-27 03:11:17 +08:00
|
|
|
|
<!-- Row 6: Special -->
|
2026-01-26 21:10:42 +08:00
|
|
|
|
{{ 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">
|
2026-01-27 00:57:35 +08:00
|
|
|
|
<span>🔬</span> 深层能力维度 (Deep Capabilities Breakdown)
|
2026-01-26 21:10:42 +08:00
|
|
|
|
</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">
|
2026-01-27 17:53:09 +08:00
|
|
|
|
{{ 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}') }}
|
2026-01-26 21:10:42 +08:00
|
|
|
|
{{ 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') }}
|
2026-01-27 00:57:35 +08:00
|
|
|
|
{{ detail_item('Loss Streak KD (连败KD)', features['hps_losing_streak_kd_diff'], 'hps_losing_streak_kd_diff') }}
|
2026-01-26 21:10:42 +08:00
|
|
|
|
|
|
|
|
|
|
{{ 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') }}
|
2026-01-27 00:57:35 +08:00
|
|
|
|
{{ detail_item('Pistol Util Eff (手枪道具)', features['ptl_pistol_util_efficiency'], 'ptl_pistol_util_efficiency', '{:.1%}') }}
|
2026-01-26 21:10:42 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-27 00:57:35 +08:00
|
|
|
|
<!-- Group 3: UTIL (Utility) -->
|
2026-01-26 21:10:42 +08:00
|
|
|
|
<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">
|
2026-01-27 00:57:35 +08:00
|
|
|
|
UTIL (Utility Usage)
|
2026-01-26 21:10:42 +08:00
|
|
|
|
</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>
|
2026-01-27 00:57:35 +08:00
|
|
|
|
|
2026-01-27 21:26:07 +08:00
|
|
|
|
<!-- 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>
|
|
|
|
|
|
|
2026-01-28 01:20:26 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
<div class="h-40">
|
|
|
|
|
|
<canvas id="phaseChart"></canvas>
|
|
|
|
|
|
</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>
|
|
|
|
|
|
|
2026-01-27 03:11:17 +08:00
|
|
|
|
<!-- 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>
|
2026-01-27 17:53:09 +08:00
|
|
|
|
{% set matches = l2_stats.get('matches', 0) or 1 %}
|
2026-01-27 03:11:17 +08:00
|
|
|
|
{% 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">
|
2026-01-27 17:53:09 +08:00
|
|
|
|
{% 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) }}
|
2026-01-27 03:11:17 +08:00
|
|
|
|
|
|
|
|
|
|
{% 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) %}
|
|
|
|
|
|
|
2026-01-27 17:53:09 +08:00
|
|
|
|
{{ 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) }}
|
2026-01-27 03:11:17 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-27 00:57:35 +08:00
|
|
|
|
<!-- 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>
|
|
|
|
|
|
|
2026-01-27 03:11:17 +08:00
|
|
|
|
{% macro vs_item_val(label, t_val, ct_val, format_str='{:.2f}') %}
|
2026-01-27 00:57:35 +08:00
|
|
|
|
{% 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 %}
|
|
|
|
|
|
|
2026-01-27 03:11:17 +08:00
|
|
|
|
{% 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 %}
|
|
|
|
|
|
|
2026-01-27 00:57:35 +08:00
|
|
|
|
<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%}') }}
|
2026-01-27 03:11:17 +08:00
|
|
|
|
|
|
|
|
|
|
{# 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%}') }}
|
2026-01-27 00:57:35 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-27 16:51:53 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 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>
|
2026-01-26 21:10:42 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-26 18:36:47 +08:00
|
|
|
|
<!-- 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>
|
2026-01-26 02:13:06 +08:00
|
|
|
|
</div>
|
2026-01-26 18:36:47 +08:00
|
|
|
|
<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>
|
2026-01-26 02:13:06 +08:00
|
|
|
|
</div>
|
2026-01-26 18:36:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-27 00:57:35 +08:00
|
|
|
|
<!-- 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">
|
2026-01-26 18:36:47 +08:00
|
|
|
|
<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>
|
2026-01-26 02:13:06 +08:00
|
|
|
|
</div>
|
2026-01-26 18:36:47 +08:00
|
|
|
|
<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>
|
2026-01-26 02:13:06 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-26 18:36:47 +08:00
|
|
|
|
{% else %}
|
|
|
|
|
|
<div class="text-center py-8 text-gray-400 text-sm">No comments yet.</div>
|
|
|
|
|
|
{% endfor %}
|
2026-01-26 02:13:06 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-26 18:36:47 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 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>
|
2026-01-27 00:57:35 +08:00
|
|
|
|
</div>
|
2026-01-26 02:13:06 +08:00
|
|
|
|
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
{% block scripts %}
|
|
|
|
|
|
<script>
|
2026-01-26 18:36:47 +08:00
|
|
|
|
let trendChartInstance = null;
|
|
|
|
|
|
|
|
|
|
|
|
function resetZoom() {
|
|
|
|
|
|
if (trendChartInstance) {
|
|
|
|
|
|
trendChartInstance.resetZoom();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 02:13:06 +08:00
|
|
|
|
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 => {
|
2026-01-26 18:36:47 +08:00
|
|
|
|
// Register Zoom Plugin Manually if needed (usually auto-registers in UMD)
|
|
|
|
|
|
if (window.ChartZoom) {
|
|
|
|
|
|
Chart.register(window.ChartZoom);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-26 02:13:06 +08:00
|
|
|
|
// Radar Chart
|
|
|
|
|
|
const ctxRadar = document.getElementById('radarChart').getContext('2d');
|
2026-01-26 21:10:42 +08:00
|
|
|
|
|
|
|
|
|
|
// Prepare Distribution Data
|
|
|
|
|
|
const dist = data.radar_dist || {};
|
|
|
|
|
|
const getDist = (key) => dist[key] || { rank: '?', avg: 0 };
|
|
|
|
|
|
|
|
|
|
|
|
// Map friendly names to keys
|
2026-01-27 21:26:07 +08:00
|
|
|
|
const keys = ['score_bat', 'score_hps', 'score_ptl', 'score_tct', 'score_util', 'score_sta', 'score_eco', 'score_pace'];
|
2026-01-26 21:10:42 +08:00
|
|
|
|
// Corresponding Labels
|
2026-01-27 21:26:07 +08:00
|
|
|
|
const rawLabels = ['Aim (BAT)', 'Clutch (HPS)', 'Pistol (PTL)', 'Defense (SIDE)', 'Util (UTIL)', 'Stability (STA)', 'Economy (ECO)', 'Pace (PACE)'];
|
2026-01-26 21:10:42 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2026-01-26 02:13:06 +08:00
|
|
|
|
new Chart(ctxRadar, {
|
|
|
|
|
|
type: 'radar',
|
|
|
|
|
|
data: {
|
2026-01-26 18:36:47 +08:00
|
|
|
|
// Update labels to friendly names
|
2026-01-26 21:10:42 +08:00
|
|
|
|
labels: labels,
|
2026-01-26 02:13:06 +08:00
|
|
|
|
datasets: [{
|
2026-01-26 21:10:42 +08:00
|
|
|
|
label: 'Player',
|
2026-01-26 02:13:06 +08:00
|
|
|
|
data: [
|
2026-01-26 18:36:47 +08:00
|
|
|
|
data.radar.BAT, data.radar.HPS,
|
|
|
|
|
|
data.radar.PTL, data.radar.SIDE, data.radar.UTIL,
|
2026-01-27 21:26:07 +08:00
|
|
|
|
data.radar.STA, data.radar.ECO, data.radar.PACE
|
2026-01-26 02:13:06 +08:00
|
|
|
|
],
|
|
|
|
|
|
backgroundColor: 'rgba(124, 58, 237, 0.2)',
|
2026-01-26 18:36:47 +08:00
|
|
|
|
borderColor: '#7c3aed',
|
|
|
|
|
|
borderWidth: 2,
|
|
|
|
|
|
pointBackgroundColor: '#7c3aed',
|
|
|
|
|
|
pointBorderColor: '#fff',
|
|
|
|
|
|
pointHoverBackgroundColor: '#fff',
|
|
|
|
|
|
pointHoverBorderColor: '#7c3aed'
|
2026-01-26 21:10:42 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: 'Team Avg',
|
|
|
|
|
|
data: teamAvgs,
|
|
|
|
|
|
backgroundColor: 'rgba(148, 163, 184, 0.2)', // Slate-400
|
|
|
|
|
|
borderColor: '#94a3b8',
|
|
|
|
|
|
borderWidth: 2,
|
|
|
|
|
|
pointRadius: 0,
|
|
|
|
|
|
borderDash: [5, 5]
|
2026-01-26 02:13:06 +08:00
|
|
|
|
}]
|
|
|
|
|
|
},
|
|
|
|
|
|
options: {
|
2026-01-26 18:36:47 +08:00
|
|
|
|
plugins: {
|
2026-01-26 21:10:42 +08:00
|
|
|
|
legend: { display: true, position: 'bottom' }
|
2026-01-26 18:36:47 +08:00
|
|
|
|
},
|
2026-01-26 02:13:06 +08:00
|
|
|
|
scales: {
|
|
|
|
|
|
r: {
|
|
|
|
|
|
beginAtZero: true,
|
2026-01-26 21:10:42 +08:00
|
|
|
|
suggestedMax: 100,
|
2026-01-26 18:36:47 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-01-26 02:13:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Trend Chart
|
|
|
|
|
|
const ctxTrend = document.getElementById('trendChart').getContext('2d');
|
2026-01-26 18:36:47 +08:00
|
|
|
|
|
|
|
|
|
|
// 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, {
|
2026-01-26 02:13:06 +08:00
|
|
|
|
type: 'line',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
labels: data.trend.labels,
|
2026-01-26 18:36:47 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
2026-01-26 02:13:06 +08:00
|
|
|
|
},
|
|
|
|
|
|
options: {
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
maintainAspectRatio: false,
|
2026-01-26 18:36:47 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-01-26 02:13:06 +08:00
|
|
|
|
scales: {
|
|
|
|
|
|
y: {
|
2026-01-26 18:36:47 +08:00
|
|
|
|
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 }
|
|
|
|
|
|
}
|
2026-01-26 02:13:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-01-28 01:20:26 +08:00
|
|
|
|
|
|
|
|
|
|
const phaseCanvas = document.getElementById('phaseChart');
|
|
|
|
|
|
if (phaseCanvas) {
|
|
|
|
|
|
const ctxPhase = phaseCanvas.getContext('2d');
|
|
|
|
|
|
new Chart(ctxPhase, {
|
|
|
|
|
|
type: 'bar',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
labels: ['Early', 'Mid', 'Late'],
|
|
|
|
|
|
datasets: [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: 'Kills',
|
|
|
|
|
|
data: [
|
|
|
|
|
|
{{ features.get('rd_phase_kill_early_share', 0) }},
|
|
|
|
|
|
{{ features.get('rd_phase_kill_mid_share', 0) }},
|
|
|
|
|
|
{{ features.get('rd_phase_kill_late_share', 0) }}
|
|
|
|
|
|
],
|
|
|
|
|
|
backgroundColor: 'rgba(124, 58, 237, 0.55)'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: 'Deaths',
|
|
|
|
|
|
data: [
|
|
|
|
|
|
{{ features.get('rd_phase_death_early_share', 0) }},
|
|
|
|
|
|
{{ features.get('rd_phase_death_mid_share', 0) }},
|
|
|
|
|
|
{{ features.get('rd_phase_death_late_share', 0) }}
|
|
|
|
|
|
],
|
|
|
|
|
|
backgroundColor: 'rgba(148, 163, 184, 0.55)'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
},
|
|
|
|
|
|
options: {
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
maintainAspectRatio: false,
|
|
|
|
|
|
scales: {
|
|
|
|
|
|
y: {
|
|
|
|
|
|
beginAtZero: true,
|
|
|
|
|
|
suggestedMax: 1,
|
|
|
|
|
|
ticks: {
|
|
|
|
|
|
callback: (v) => `${Math.round(v * 100)}%`
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
plugins: {
|
|
|
|
|
|
legend: { display: true, position: 'bottom' },
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
callbacks: {
|
|
|
|
|
|
label: (ctx) => `${ctx.dataset.label}: ${(ctx.parsed.y * 100).toFixed(1)}%`
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-26 02:13:06 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
2026-01-28 01:20:26 +08:00
|
|
|
|
{% endblock %}
|