Fase 3 (isla): dashboard de «Mi cuenta» en React/TSX

De las ~508 líneas de partials/my-account.html, solo la parte superior es dinámica
(datos de cuenta, estado, puntos, token, personajes); el resto son paneles de
navegación estáticos (enlaces), que se quedan en Django.

- API: account_me_api -> /api/account/me/ (protegido por sesión, 401 si no hay):
  user_info, account_status, dp/vp, créditos Battlepay, token de seguridad y
  personajes. Reutiliza get_account_characters/get_account_status/formatear_fecha.
  Resiliente si acore no está disponible.
- React: AccountDashboard.tsx (fieldsets de datos + estado con contador de baneo en
  vivo + rejilla de personajes). Entry my_account.
- partials/my-account.html: los 2 fieldsets + la rejilla de personajes + el script
  del contador se sustituyen por #account-dashboard-app; los paneles de utilidades
  e historiales siguen en Django.

Verificado: check OK, /api/account/me/ 401 sin sesión, my-account redirige a login.
(El render con datos reales requiere sesión + BD AzerothCore.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 21:23:17 +00:00
parent cac0d28aa9
commit b2069df4c3
6 changed files with 241 additions and 102 deletions
+1
View File
@@ -6,4 +6,5 @@ urlpatterns = [
path('home/news/', views.home_news_api, name='api_home_news'),
path('home/status/', views.home_status_api, name='api_home_status'),
path('account/select/', views.account_select_api, name='api_account_select'),
path('account/me/', views.account_me_api, name='api_account_me'),
]
+4 -102
View File
@@ -1,3 +1,4 @@
{% load django_vite %}
<div class="main-page">
<div class="middle-content">
<div class="body-content">
@@ -6,89 +7,11 @@
<div class="title-box-content"><h2>Información</h2></div>
<div class="body-box-content">
<script src="{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-js-handlers/my_account_response.js"></script>
<fieldset class="account-fieldset">
<legend>Datos básicos</legend>
<div class="separate">
<p>Cuenta (Battle.net): <span>{{ request.session.bnet_email|default:user_info.email }}</span></p>
<p>Cuenta de juego: <span>{{ user_info.username }}</span></p>
<p>Correo de registro: <span>{{ user_info.reg_mail }}</span></p>
<p>Correo actual: <span>{{ user_info.email }}</span></p>
<p>Fecha de registro: <span>{{ user_info.joindate }}</span></p>
<p>Última IP (web): <span>{{ user_info.last_ip }}</span></p>
<p>Última IP (Servidor): <span>{{ user_info.last_attempt_ip }}</span></p>
</div>
</fieldset>
<fieldset class="account-fieldset">
<legend>Estado de la cuenta</legend>
<div class="separate">
<img src="{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-ranks/1_Newbie.svg" class="nw-rank" alt="Nivel 1" title="Nivel 1">
<p>PD: <span>{{ dp }}</span></p>
<p>PV: <span>{{ vp }}</span></p>
<p>Créditos Battlepay: <span>{{ battlepay_credits }}</span></p>
<p>Token de Seguridad:
<span>
{% if token_status == "Solicitado" %}
{{ token_status }} - {{ token_date }}
{% else %}
{{ token_status }}
{% endif %}
</span>
</p>
{% if account_status.is_banned %}
<p>Cuenta baneada: <span></span></p>
<p>Fin de la suspensión: <span>{{ account_status.unban_date }}</span></p>
<p id="ban-timer">Tiempo restante: <span id="remaining-time">{{ account_status.remaining_time }}</span></p>
{% else %}
<p>Cuenta baneada: <span>No</span> (<a href="ban-history">Consultar historial</a>)</p>
{% if account_status.is_recruited %}
<p>Cuenta reclutada: <span></span></p>
{% else %}
<p>Cuenta reclutada: <span>No</span></p>
{% endif %}
<p>Amigos reclutados: <span>{{ account_status.recruited_count }}</span></p>
{% endif %}
</div>
</fieldset>
<div id="account-dashboard-app"></div>
{% vite_asset 'src/entries/my_account.tsx' %}
</div>
</div>
{% if has_characters %}
<div class="box-content">
<div class="title-box-content">
<h2>Mis personajes</h2>
</div>
<div class="characters-grid">
{% for character in characters %}
<div class="char-box char-box-{{ character.class_css }}">
<div class="char-text">
<!-- Imagen del personaje basada en raza, clase y género -->
<img class="img-small-icon char-icon" src="{{ URL_PRINCIPAL }}/static/{{ character.image_url }}" alt="{{ character.name }}">
<!-- Información del personaje -->
<p class="{{ character.class_css }} big-font">{{ character.name }}</p>
<p><span>Nivel {{ character.level }}</span></p>
<!-- Mostrar zona del personaje -->
<p class="second-brown">
{{ character.zone }}
</p>
<!-- Mostrar monedas del personaje -->
<p>
<span>
{{ character.gold }}<img src="{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-icons/money-gold.gif" width="10px">
{{ character.silver }}<img src="{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-icons/money-silver.webp" width="10px">
{{ character.copper }}<img src="{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-icons/money-copper.gif" width="10px">
</span>
</p>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<style>
.title-box-content {
margin-bottom: 20px; /* Separación entre el título y la cuadrícula */
@@ -484,25 +407,4 @@
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
let remainingTime = parseInt(document.getElementById('remaining-time').textContent);
function updateBanTimer() {
if (remainingTime > 0) {
remainingTime--;
const hours = Math.floor(remainingTime / 3600);
const minutes = Math.floor((remainingTime % 3600) / 60);
const seconds = remainingTime % 60;
document.getElementById('ban-timer').textContent = `Tiempo restante: ${hours}h ${minutes}m ${seconds}s`;
if (remainingTime === 0) {
location.reload(); // Recargar la página cuando el tiempo llegue a 0
}
}
}
// Actualizar el contador cada segundo
setInterval(updateBanTimer, 1000);
});
</script>
+47
View File
@@ -1,6 +1,7 @@
from ._base import *
from .pages import check_server_status, get_online_characters_count
from .auth import _set_game_account_session
from .account import get_account_characters, get_account_status, formatear_fecha
# Endpoints JSON que alimentan las islas React/TSX del frontend (Vite).
# Se montan bajo /api/ (fuera de i18n_patterns). De momento con JsonResponse;
@@ -60,3 +61,49 @@ def account_select_api(request):
return JsonResponse({"success": False, "error": "Cuenta de juego no válida."})
_set_game_account_session(request, chosen)
return JsonResponse({"success": True, "redirect": reverse("my-account")})
def account_me_api(request):
"""Datos del panel «Mi cuenta» (isla): info de cuenta, estado, puntos,
créditos Battlepay, token de seguridad y personajes. Requiere sesión."""
username = request.session.get('username')
if not username:
return JsonResponse({"authenticated": False}, status=401)
try:
with connections['acore_auth'].cursor() as cursor:
cursor.execute(
"SELECT id, username, reg_mail, email, last_ip, last_attempt_ip, joindate "
"FROM account WHERE username = %s", [username])
row = cursor.fetchone()
if not row:
return JsonResponse({"authenticated": False}, status=404)
account_id = row[0]
characters = get_account_characters(account_id)
status = get_account_status(account_id)
points = HomeApiPoints.objects.filter(accountID=account_id).first()
sectoken = SecurityToken.objects.filter(user_id=account_id).first()
with connections['acore_auth'].cursor() as cursor:
credits = bnet.get_battlepay_credits(cursor, request.session.get('bnet_id'))
except Exception as e:
logger.error("account_me_api: %s", e)
return JsonResponse({"authenticated": True, "error": "No se pudieron cargar los datos de la cuenta."}, status=200)
return JsonResponse({
"authenticated": True,
"user_info": {
"bnet_email": request.session.get('bnet_email') or row[3],
"username": row[1],
"reg_mail": row[2],
"email": row[3],
"last_ip": row[4],
"last_attempt_ip": row[5],
"joindate": formatear_fecha(row[6]),
},
"account_status": status,
"dp": points.dp if points else 0,
"vp": points.vp if points else 0,
"battlepay_credits": credits,
"token_status": "Solicitado" if sectoken else "Sin solicitar",
"token_date": sectoken.created_at.strftime('%H:%M:%S %d-%m-%Y') if sectoken else None,
"characters": characters,
})