Marcado y estructura de las plantillas

Template Markup & Structure — How to build and customize the HTML template for your waiting room.

Véase también: Estilo y variables CSS | Estado y métodos de Vue

Primeros pasos

Abre el archivo HTML de la plantilla tanto en un editor de texto como en un navegador. La plantilla contiene un <script id="crowdhandler-data" type="application/ld+json"> etiqueta con un objeto JSON que configura la sala de espera. Para acceder al modo de vista previa —en el que puedes probar todos los estados de la cola sin conectarte a la API en producción—, añade "dev": true a este objeto:

<script id="crowdhandler-data" type="application/ld+json">
{
    "dev": true,
    "status": 1,
    "title": "Test Event",
    "position": 150,
    "estimate": 12,
    "simulatePolling": true
}
</script>

Con "dev": true Una vez configurado, la plantilla utiliza los valores de este objeto JSON en lugar de realizar llamadas reales a la API. Modifica las propiedades, guarda el archivo y actualiza el navegador para ver los distintos estados. Eliminar «dev» (o configúralo en false) para volver al modo de producción.

Preview Status Values

Estado Estado
1 Cola activa
2 Error
3 Bloqueado
4 Cuenta atrás
5 Habitación completa
100 Se requiere un captcha

Preview Examples

// Active queue
{"dev": true, "status": 1, "position": 100, "estimate": 10}

// Countdown
{"dev": true, "status": 4, "onsale": "2026-02-15T14:00:00Z"}

// Blocked
{"dev": true, "status": 3}

Para consultar la lista completa de propiedades de vista previa, consulta la referencia del modo de vista previa.


Overview

La plantilla utiliza directivas de Vue 3 para mostrar secciones de forma condicional en función del estado de la cola. El objeto de estado principal ch contiene todos los datos de la cola y se actualiza automáticamente mediante consultas a la API (por defecto, cada 60 segundos; TTL variable).

Required Elements

<!-- Container that shows when loaded -->
<div id="crowdhandler-template" :class="{ 'active': ch.isLoaded }" role="main">
  <!-- Screen reader announcements -->
  <div class="ch-sr-only" aria-live="polite" aria-atomic="true">{{ ch.announcement }}</div>

  <!-- Your template content -->

  <!-- Captcha container (required if captcha is enabled) -->
  <div id="ch-captcha-form"></div>

  <!-- Script inclusion -->
  <script type="module" src="/src/main.js"></script>
</div>

Queue States

La plantilla utiliza ch.captchaRequired como puerta de nivel superior, y a continuación comprueba otros estados dentro de la rama que no es de captcha utilizando un v-if / v-else-if cadena.

1. Captcha Required

ch.captchaRequired

El usuario debe completar el captcha antes de entrar en la cola. El widget del captcha se muestra automáticamente en #ch-captcha-form.

<div v-if="ch.captchaRequired" class="ch-captcha-section">
  <p class="ch-captcha-message">Please verify you're human to continue</p>
  <div id="ch-captcha-form"></div>
</div>
<div v-else>
  <!-- All other states go here -->
</div>

2. Blocked

ch.isBlocked

Se ha bloqueado al usuario debido a una actividad sospechosa.

<div v-if="ch.isBlocked" class="ch-state-message ch-state-danger" role="alert">
  <h3 class="ch-state-title">Access Denied</h3>
  <p class="ch-state-description">You've been blocked due to suspicious activity.</p>
</div>

3. Room Full

ch.isRoomFull

La cola ha alcanzado su capacidad máxima. Es necesario utilizar v-else-if tras el cheque bloqueado.

<div v-else-if="ch.isRoomFull" class="ch-state-message ch-state-warning" role="alert">
  <h3 class="ch-state-title">Queue Full</h3>
  <p class="ch-state-description">We're at capacity. You'll be let in when a spot opens up.</p>
</div>

4. Countdown (Pre-Queue)

ch.isCountdown

La cola aún no se ha abierto. Muestra un temporizador de cuenta atrás.

<div v-if="ch.isCountdown" class="ch-countdown">
  <p class="ch-countdown-label">Queue opens in</p>
  <div class="ch-countdown-display">
    <div class="ch-countdown-unit">
      <div class="ch-countdown-value">{{ ch.countdown.sHours }}</div>
      <div class="ch-countdown-suffix">hrs</div>
    </div>
    <div class="ch-countdown-unit">
      <div class="ch-countdown-value">{{ ch.countdown.sMinutes }}</div>
      <div class="ch-countdown-suffix">min</div>
    </div>
    <div class="ch-countdown-unit">
      <div class="ch-countdown-value">{{ ch.countdown.sSeconds }}</div>
      <div class="ch-countdown-suffix">sec</div>
    </div>
  </div>
</div>

5. Active Queue

ch.isActive

El usuario se encuentra en la cola activa y ocupa una posición. Cuando llega al principio de la cola, se le redirige automáticamente.

<div v-if="ch.isActive">
  <h2 v-if="ch.position">{{ ch.position }}</h2>
  <div v-else>Checking in!</div>
</div>

Template Sections

Header (Logo + Title)

El título adquiere una clase de tamaño dinámica en función del número de caracteres.

<header class="ch-header">
  <div class="ch-logo ch-enter ch-stagger-1" v-if="ch.logo">
    <img :src="ch.logo" :alt="ch.title" />
  </div>
  <div class="ch-header-row ch-enter ch-stagger-4">
    <h1 class="ch-title" :class="ch.css.title">{{ ch.title }}</h1>
  </div>
</header>

Message

Se muestra encima de la tarjeta. Admite saltos de línea mediante espacio en blanco: antes de la línea. Se avisa a los lectores de pantalla cuando cambia.

<div class="ch-message ch-message-box ch-enter ch-stagger-6" v-if="ch.message">
  {{ ch.message }}
</div>

Countdown + Active Queue Wrapper

Estos estados comparten un «wrapper», ya que la cola pasa del estado de cuenta atrás al estado activo.

<div v-if="ch.isCountdown || ch.isActive" class="ch-queue-states ch-enter ch-stagger-4">
  <!-- Countdown, position, progress bar, priority code, stock -->
</div>

Position Display

<div class="ch-position-display" v-if="ch.position && ch.isActive">
  <div class="ch-position-label">Your position</div>
  <h2 class="ch-position-number" :key="ch.position"><strong>{{ ch.position }}</strong></h2>
</div>

Progress Bar

Muestra el progreso de la cola cuando hay una posición disponible. Muestra una barra de progreso basada en sondeos al realizar el check-in (ch.position es null).

<!-- With position -->
<div v-if="ch.position && ch.isActive" class="ch-progress-container">
  <div class="ch-progress-bar" role="progressbar"
    :aria-valuenow="Math.round(ch.progress || 0)"
    aria-valuemin="0" aria-valuemax="100">
    <div class="ch-progress-fill"
      :style="{ width: (isNaN(ch.progress) ? 10 : ch.progress) + '%' }"></div>
  </div>
  <div class="ch-progress-info">
    <span v-if="ch.estimate && ch.estimate <= 1">Almost there!</span>
    <span v-else-if="ch.estimate && ch.estimate > 60">More than an hour</span>
    <span v-else-if="ch.estimate">Approximately {{ ch.estimate }} minutes</span>
    <span v-if="ch.eta" class="ch-eta">ETA: {{ ch.eta }}</span>
  </div>
</div>

<!-- Checking in (no position yet) -->
<div v-else-if="ch.isActive" class="ch-progress-container">
  <div class="ch-position-label ch-checking-in">Checking in!</div>
  <div class="ch-progress-bar" role="progressbar"
    :aria-valuenow="Math.round(ch.nextPoll || 0)"
    aria-valuemin="0" :aria-valuemax="ch.pollTTL">
    <div class="ch-progress-fill"
      :style="{ width: (isNaN(ch.nextPoll) ? 0 : (ch.nextPoll / ch.pollTTL * 100)) + '%' }">
    </div>
  </div>
</div>

Stock Indicator

<div v-if="ch.stock !== null" class="ch-stock-badge"
  :class="{
    'ch-stock-low': ch.stock > 0 && ch.stock <= 50,
    'ch-stock-zero': ch.stock === 0
  }">
  <span v-if="ch.stock > 0">
    <span class="ch-stock-count">{{ ch.stock }}</span> items remaining
  </span>
  <span v-else>Sold out</span>
</div>

Session Info

<!-- During countdown -->
<div v-if="ch.isCountdown" class="ch-session-info">
  Stay on this page. You'll get a random position when the queue opens.
</div>

<!-- During active queue -->
<div v-if="ch.isActive">
  <span v-if="ch.sessionsExpire" class="ch-session-info">
    Keep this window open. You'll move forward automatically.
  </span>
  <span v-else-if="ch.sessionsTimeout" class="ch-session-info">
    You'll be redirected when it's your turn.
    You have {{ ch.sessionsTimeout }} minutes to complete your transaction.
  </span>
</div>

Email Notification Form

Tres estados: formulario de introducción de datos, control deslizante de envío y confirmación. Los usuarios pueden modificar su correo electrónico tras el envío. Solo se muestra mientras la cola está activa.

<div v-if="ch.emailAvailable && ch.isActive" class="ch-form-section ch-email-section">
  <!-- Submitting -->
  <div v-if="ch.email.submitting" class="ch-email-submitting">
    <div class="ch-spinner" role="status" aria-label="Submitting"></div>
  </div>

  <!-- Form -->
  <div v-else-if="!ch.email.submitted || ch.email.changing">
    <div v-if="ch.email.changing" class="ch-form-title">Change notification email</div>
    <div v-else class="ch-form-title">Get notified when it's your turn</div>
    <div class="ch-input-group">
      <input type="email" class="ch-input" placeholder="your@email.com"
        v-model="ch.email.value" @keyup="validateEmail" />
      <button class="ch-btn" @click="submitEmail" :disabled="!ch.email.valid">Notify me</button>
      <button v-if="ch.email.changing" class="ch-btn ch-btn-secondary"
        @click="cancelChangeEmail">Cancel</button>
    </div>
  </div>

  <!-- Confirmation -->
  <div v-else role="status">
    <div class="ch-form-feedback ch-success">We'll email {{ ch.email.value }}</div>
    <button @click="changeEmail" class="ch-btn ch-btn-sm ch-btn-change">Change</button>
  </div>
</div>

Priority Code Form

Disponible tanto durante la cuenta atrás como mientras se está en la cola activa.

<div v-if="ch.priority && ch.priority.available" class="ch-form-section ch-priority-section">
  <div class="ch-form-title">Got a priority code?</div>
  <div class="ch-input-group">
    <input type="text" class="ch-input" id="ch-priority-input"
      placeholder="Enter code" v-model="ch.priority.code"
      @keyup="checkValidPriorityCode" />
    <button class="ch-btn" @click="prioritySubmit"
      :disabled="ch.priority.submitting || !ch.priority.code">Apply</button>
  </div>
  <div v-if="ch.priority.submitted && ch.priority.success"
    class="ch-form-feedback ch-success" role="status">Code applied!</div>
  <div v-if="ch.priority.submitted && ch.priority.error"
    class="ch-form-feedback ch-error" role="status">Invalid code</div>
</div>

Footer

Diseño de tres columnas: hora del último inicio de sesión (izquierda), token de sesión (centro), «desarrollado por» (derecha). Oculto durante el captcha.

<div class="ch-footer ch-enter ch-stagger-5" v-if="!ch.captchaRequired">
  <div class="ch-footer-left">
    <div class="ch-footer-meta" v-if="ch.requested">
      <span>Last check in:</span>
      <code class="ch-footer-meta-value">
        <span :class="{ 'ch-text-shimmer': ch.nextPoll < 3 }">
          {{ formatDate(ch.requested, 'toLocaleTimeString') }}
        </span>
      </code>
    </div>
  </div>
  <div class="ch-footer-center">
    <div class="ch-token-display" v-if="ch.token.obfuscated || ch.token.value">
      <span>ID:</span>
      <code class="ch-token-value" tabindex="0" role="button"
        @click="(e) => ch.token.copy(e, ch)">
        {{ ch.token.copied ? 'Copied!' : (ch.token.obfuscated || ch.token.value) }}
      </code>
    </div>
  </div>
  <div class="ch-powered-by">
    Powered by <a href="https://www.crowdhandler.com/..." target="_blank">CrowdHandler</a>
  </div>
</div>

Customization Philosophy

El index.html La plantilla es una implementación de referencia: incluye todas las funciones que admite el sistema. No es necesario que tu plantilla la reproduzca íntegramente.

Keep the features, change the design

Cada sección es opcional, pero recomendamos mantener todas las funcionalidades en tu plantilla. La aplicación no dejará de funcionar si eliminas el código de una función; simplemente no se mostrará la interfaz de usuario correspondiente. Sin embargo, es mucho más fácil tener el código ya preparado cuando necesites activar una función durante una venta en directo. Por ejemplo, si un cliente decide activar el captcha o la visualización de existencias en mitad de un evento, la plantilla lo muestra inmediatamente sin necesidad de más cambios. Sin el código de marcado, tendrías que actualizar y volver a implementar la plantilla, y el almacenamiento en caché de la plantilla implica que esa actualización no surtirá efecto de inmediato.

Los únicos requisitos imprescindibles son los siguientes: #plantilla-gestor-de-multitudes el contenedor y el archivo de script.

Reshape the state logic

Puedes reorganizar las directivas de Vue como quieras. Podrías consultar ch.stock === 0 En primer lugar, puedes mostrar un panel a pantalla completa con el mensaje «Agotado», combinar la cuenta atrás con un mensaje de avance o integrar el formulario del código de prioridad dentro de la cuenta atrás. La plantilla de referencia agrupa los estados en una jerarquía concreta, pero puedes reestructurarla como prefieras.

Build around the data, not the markup

Piensa en ch como una API de datos, en lugar de un diseño predefinido. Las propiedades de estado son valores reactivos que puedes utilizar en cualquier lugar, en cualquier combinación y con cualquier lógica condicional que tenga sentido para tu diseño. La plantilla de referencia es un punto de partida, no una restricción.