You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1711 lines
45 KiB
Vue
1711 lines
45 KiB
Vue
|
2 months ago
|
<!-- components/AdminAcademia/Cursos/PreguntasCursoView.vue -->
|
||
|
|
<template>
|
||
|
|
<div class="preguntas-curso-view">
|
||
|
|
<!-- Header con navegación -->
|
||
|
|
<div class="view-header">
|
||
|
|
<div class="header-left">
|
||
|
|
<a-button type="link" @click="goBack" class="back-btn">
|
||
|
|
<ArrowLeftOutlined /> Volver a Cursos
|
||
|
|
</a-button>
|
||
|
|
<div class="header-title">
|
||
|
|
<h2>{{ curso.nombre }}</h2>
|
||
|
|
<div class="curso-info">
|
||
|
|
<span class="curso-codigo">{{ curso.codigo }}</span>
|
||
|
|
<a-tag :color="curso.activo ? 'green' : 'red'">
|
||
|
|
{{ curso.activo ? 'Activo' : 'Inactivo' }}
|
||
|
|
</a-tag>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="header-right">
|
||
|
|
<a-button type="primary" @click="showCreateModal">
|
||
|
|
<template #icon><PlusOutlined /></template>
|
||
|
|
Nueva Pregunta
|
||
|
|
</a-button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Estadísticas del Curso -->
|
||
|
|
<div class="curso-stats">
|
||
|
|
<a-card class="stat-card">
|
||
|
|
<div class="stat-content">
|
||
|
|
<div class="stat-icon-wrapper" style="background: #e6f7ff;">
|
||
|
|
<QuestionCircleOutlined style="color: #1890ff;" />
|
||
|
|
</div>
|
||
|
|
<div class="stat-info">
|
||
|
|
<div class="stat-value">{{ estadisticas.total || 0 }}</div>
|
||
|
|
<div class="stat-label">Total Preguntas</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</a-card>
|
||
|
|
|
||
|
|
<a-card class="stat-card">
|
||
|
|
<div class="stat-content">
|
||
|
|
<div class="stat-icon-wrapper" style="background: #f6ffed;">
|
||
|
|
<CheckCircleOutlined style="color: #52c41a;" />
|
||
|
|
</div>
|
||
|
|
<div class="stat-info">
|
||
|
|
<div class="stat-value">{{ estadisticas.facil || 0 }}</div>
|
||
|
|
<div class="stat-label">Fáciles</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</a-card>
|
||
|
|
|
||
|
|
<a-card class="stat-card">
|
||
|
|
<div class="stat-content">
|
||
|
|
<div class="stat-icon-wrapper" style="background: #fff7e6;">
|
||
|
|
<WarningOutlined style="color: #fa8c16;" />
|
||
|
|
</div>
|
||
|
|
<div class="stat-info">
|
||
|
|
<div class="stat-value">{{ estadisticas.medio || 0 }}</div>
|
||
|
|
<div class="stat-label">Medias</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</a-card>
|
||
|
|
|
||
|
|
<a-card class="stat-card">
|
||
|
|
<div class="stat-content">
|
||
|
|
<div class="stat-icon-wrapper" style="background: #fff2f0;">
|
||
|
|
<CloseCircleOutlined style="color: #ff4d4f;" />
|
||
|
|
</div>
|
||
|
|
<div class="stat-info">
|
||
|
|
<div class="stat-value">{{ estadisticas.dificil || 0 }}</div>
|
||
|
|
<div class="stat-label">Difíciles</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</a-card>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Filtros -->
|
||
|
|
<div class="filters-section">
|
||
|
|
<div class="filters">
|
||
|
|
<a-input-search
|
||
|
|
v-model:value="searchText"
|
||
|
|
placeholder="Buscar en preguntas..."
|
||
|
|
@search="handleSearch"
|
||
|
|
style="width: 300px"
|
||
|
|
size="large"
|
||
|
|
/>
|
||
|
|
|
||
|
|
<a-select
|
||
|
|
v-model:value="dificultadFilter"
|
||
|
|
placeholder="Todas las dificultades"
|
||
|
|
style="width: 200px; margin-left: 16px"
|
||
|
|
size="large"
|
||
|
|
@change="handleFilterChange"
|
||
|
|
>
|
||
|
|
<a-select-option value="">Todas las dificultades</a-select-option>
|
||
|
|
<a-select-option value="facil">Fácil</a-select-option>
|
||
|
|
<a-select-option value="medio">Media</a-select-option>
|
||
|
|
<a-select-option value="dificil">Difícil</a-select-option>
|
||
|
|
</a-select>
|
||
|
|
|
||
|
|
<a-select
|
||
|
|
v-model:value="estadoFilter"
|
||
|
|
placeholder="Todos los estados"
|
||
|
|
style="width: 200px; margin-left: 16px"
|
||
|
|
size="large"
|
||
|
|
@change="handleFilterChange"
|
||
|
|
>
|
||
|
|
<a-select-option value="">Todos los estados</a-select-option>
|
||
|
|
<a-select-option value="true">Activo</a-select-option>
|
||
|
|
<a-select-option value="false">Inactivo</a-select-option>
|
||
|
|
</a-select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="filter-actions">
|
||
|
|
<a-button @click="clearFilters" size="large">
|
||
|
|
<template #icon><ReloadOutlined /></template>
|
||
|
|
Limpiar filtros
|
||
|
|
</a-button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Tabla de Preguntas -->
|
||
|
|
<div class="preguntas-table-container">
|
||
|
|
<a-table
|
||
|
|
:data-source="preguntas"
|
||
|
|
:columns="columns"
|
||
|
|
:loading="preguntaStore.loading"
|
||
|
|
:pagination="pagination"
|
||
|
|
row-key="id"
|
||
|
|
@change="handleTableChange"
|
||
|
|
class="preguntas-table"
|
||
|
|
>
|
||
|
|
<template #bodyCell="{ column, record }">
|
||
|
|
<!-- Dificultad -->
|
||
|
|
<template v-if="column.key === 'nivel_dificultad'">
|
||
|
|
<a-tag :color="getDificultadColor(record.nivel_dificultad)">
|
||
|
|
{{ formatDificultad(record.nivel_dificultad) }}
|
||
|
|
</a-tag>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<!-- Estado -->
|
||
|
|
<template v-if="column.key === 'activo'">
|
||
|
|
<a-switch
|
||
|
|
:checked="record.activo"
|
||
|
|
@change="togglePreguntaStatus(record)"
|
||
|
|
:checked-children="'Activo'"
|
||
|
|
:un-checked-children="'Inactivo'"
|
||
|
|
/>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<!-- Vista Previa del Enunciado -->
|
||
|
|
<template v-if="column.key === 'enunciado'">
|
||
|
|
<div class="enunciado-preview" v-html="getEnunciadoPreview(record.enunciado)" />
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<!-- Fecha Creación -->
|
||
|
|
<template v-if="column.key === 'created_at'">
|
||
|
|
{{ formatDate(record.created_at) }}
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<!-- Acciones -->
|
||
|
|
<template v-if="column.key === 'acciones'">
|
||
|
|
<a-space>
|
||
|
|
<a-button
|
||
|
|
type="link"
|
||
|
|
size="small"
|
||
|
|
@click="verPregunta(record)"
|
||
|
|
class="action-btn"
|
||
|
|
>
|
||
|
|
<EyeOutlined /> Ver
|
||
|
|
</a-button>
|
||
|
|
<a-button
|
||
|
|
type="link"
|
||
|
|
size="small"
|
||
|
|
@click="showEditModal(record)"
|
||
|
|
class="action-btn"
|
||
|
|
>
|
||
|
|
<EditOutlined /> Editar
|
||
|
|
</a-button>
|
||
|
|
<a-button
|
||
|
|
type="link"
|
||
|
|
size="small"
|
||
|
|
danger
|
||
|
|
@click="confirmDeletePregunta(record)"
|
||
|
|
class="action-btn"
|
||
|
|
>
|
||
|
|
<DeleteOutlined /> Eliminar
|
||
|
|
</a-button>
|
||
|
|
</a-space>
|
||
|
|
</template>
|
||
|
|
</template>
|
||
|
|
</a-table>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Modal Crear/Editar Pregunta -->
|
||
|
|
<a-modal
|
||
|
|
v-model:open="modalPreguntaVisible"
|
||
|
|
:title="isEditingPregunta ? 'Editar Pregunta' : 'Nueva Pregunta'"
|
||
|
|
:confirm-loading="preguntaStore.loading"
|
||
|
|
@ok="handleModalPreguntaOk"
|
||
|
|
@cancel="handleModalPreguntaCancel"
|
||
|
|
width="900px"
|
||
|
|
class="pregunta-modal"
|
||
|
|
>
|
||
|
|
<a-form
|
||
|
|
ref="formPreguntaRef"
|
||
|
|
:model="formPreguntaState"
|
||
|
|
:rules="formPreguntaRules"
|
||
|
|
layout="vertical"
|
||
|
|
>
|
||
|
|
<!-- Sección del Enunciado -->
|
||
|
|
<a-card size="small" class="section-card">
|
||
|
|
<template #title>
|
||
|
|
<div class="section-title">
|
||
|
|
<span>Enunciado de la Pregunta</span>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<a-form-item
|
||
|
|
label="Texto del Enunciado *"
|
||
|
|
name="enunciado"
|
||
|
|
:validate-status="getFieldStatus('enunciado')"
|
||
|
|
:help="getFieldHelp('enunciado')"
|
||
|
|
>
|
||
|
|
<a-textarea
|
||
|
|
v-model:value="formPreguntaState.enunciado"
|
||
|
|
placeholder="Escribe el enunciado principal de la pregunta..."
|
||
|
|
:rows="4"
|
||
|
|
:maxlength="1000"
|
||
|
|
show-count
|
||
|
|
/>
|
||
|
|
</a-form-item>
|
||
|
|
|
||
|
|
<!-- Subida de imágenes para el enunciado -->
|
||
|
|
<a-form-item
|
||
|
|
label="Imágenes del Enunciado (Opcional)"
|
||
|
|
>
|
||
|
|
<div class="upload-section">
|
||
|
|
<a-upload
|
||
|
|
v-model:file-list="imagenesEnunciadoFiles"
|
||
|
|
list-type="picture-card"
|
||
|
|
:multiple="true"
|
||
|
|
:before-upload="beforeUpload"
|
||
|
|
accept="image/*"
|
||
|
|
@preview="handlePreview"
|
||
|
|
@remove="handleRemoveImagenEnunciado"
|
||
|
|
>
|
||
|
|
<div v-if="imagenesEnunciadoFiles.length < 5">
|
||
|
|
<PlusOutlined />
|
||
|
|
<div class="ant-upload-text">Agregar Imagen</div>
|
||
|
|
<div class="ant-upload-hint">Máx. 5 imágenes, 2MB c/u</div>
|
||
|
|
</div>
|
||
|
|
</a-upload>
|
||
|
|
|
||
|
|
<!-- Vista previa de imágenes existentes (solo en edición) -->
|
||
|
|
<div v-if="isEditingPregunta && formPreguntaState.imagenes_existentes?.length" class="existing-images">
|
||
|
|
<h4>Imágenes existentes:</h4>
|
||
|
|
<div class="image-grid">
|
||
|
|
<div v-for="(imagen, index) in formPreguntaState.imagenes_existentes"
|
||
|
|
:key="'existente-'+index"
|
||
|
|
class="image-item">
|
||
|
|
<img :src="getImageUrl(imagen)" :alt="'Imagen ' + (index+1)" />
|
||
|
|
<a-button
|
||
|
|
type="link"
|
||
|
|
danger
|
||
|
|
@click="eliminarImagenExistente('enunciado', index)"
|
||
|
|
class="remove-image-btn"
|
||
|
|
>
|
||
|
|
<DeleteOutlined />
|
||
|
|
</a-button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</a-form-item>
|
||
|
|
|
||
|
|
<a-form-item
|
||
|
|
label="Enunciado Adicional (Opcional)"
|
||
|
|
>
|
||
|
|
<a-textarea
|
||
|
|
v-model:value="formPreguntaState.enunciado_adicional"
|
||
|
|
placeholder="Información adicional, contexto o detalles importantes..."
|
||
|
|
:rows="3"
|
||
|
|
/>
|
||
|
|
</a-form-item>
|
||
|
|
</a-card>
|
||
|
|
|
||
|
|
<!-- Sección de Opciones -->
|
||
|
|
<a-card size="small" class="section-card">
|
||
|
|
<template #title>
|
||
|
|
<div class="section-title">
|
||
|
|
<span>Opciones de Respuesta</span>
|
||
|
|
<span class="section-subtitle">(Mínimo 2 opciones, selecciona la correcta)</span>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<div class="opciones-container">
|
||
|
|
<div v-for="(opcion, index) in formPreguntaState.opciones"
|
||
|
|
:key="index"
|
||
|
|
class="opcion-item"
|
||
|
|
:class="{ 'correct-option': formPreguntaState.respuesta_correcta === opcion }">
|
||
|
|
<div class="opcion-header">
|
||
|
|
<div class="opcion-label">
|
||
|
|
<span class="opcion-letter">{{ String.fromCharCode(65 + index) }}</span>
|
||
|
|
<a-input
|
||
|
|
v-model:value="formPreguntaState.opciones[index]"
|
||
|
|
placeholder="Texto de la opción..."
|
||
|
|
@change="updateOpcion(index, $event.target.value)"
|
||
|
|
class="opcion-input"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div class="opcion-actions">
|
||
|
|
<a-radio
|
||
|
|
:checked="formPreguntaState.respuesta_correcta === opcion"
|
||
|
|
@change="() => setRespuestaCorrecta(opcion)"
|
||
|
|
>
|
||
|
|
Correcta
|
||
|
|
</a-radio>
|
||
|
|
<a-button
|
||
|
|
v-if="formPreguntaState.opciones.length > 2"
|
||
|
|
type="link"
|
||
|
|
danger
|
||
|
|
@click="removeOpcion(index)"
|
||
|
|
class="remove-opcion-btn"
|
||
|
|
>
|
||
|
|
<DeleteOutlined /> Eliminar
|
||
|
|
</a-button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<a-button
|
||
|
|
type="dashed"
|
||
|
|
@click="addOpcion"
|
||
|
|
block
|
||
|
|
:disabled="formPreguntaState.opciones.length >= 5"
|
||
|
|
>
|
||
|
|
<PlusOutlined /> Agregar Opción ({{ formPreguntaState.opciones.length }}/5)
|
||
|
|
</a-button>
|
||
|
|
</a-card>
|
||
|
|
|
||
|
|
<!-- Sección de Explicación -->
|
||
|
|
<a-card size="small" class="section-card">
|
||
|
|
<template #title>
|
||
|
|
<div class="section-title">
|
||
|
|
<span>Explicación y Retroalimentación</span>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<a-form-item
|
||
|
|
label="Explicación de la Respuesta Correcta (Opcional)"
|
||
|
|
>
|
||
|
|
<a-textarea
|
||
|
|
v-model:value="formPreguntaState.explicacion"
|
||
|
|
placeholder="Explica por qué esta opción es la correcta..."
|
||
|
|
:rows="4"
|
||
|
|
/>
|
||
|
|
</a-form-item>
|
||
|
|
|
||
|
|
<!-- Subida de imágenes para la explicación -->
|
||
|
|
<a-form-item
|
||
|
|
label="Imágenes de la Explicación (Opcional)"
|
||
|
|
>
|
||
|
|
<div class="upload-section">
|
||
|
|
<a-upload
|
||
|
|
v-model:file-list="imagenesExplicacionFiles"
|
||
|
|
list-type="picture-card"
|
||
|
|
:multiple="true"
|
||
|
|
:before-upload="beforeUpload"
|
||
|
|
accept="image/*"
|
||
|
|
@preview="handlePreview"
|
||
|
|
@remove="handleRemoveImagenExplicacion"
|
||
|
|
>
|
||
|
|
<div v-if="imagenesExplicacionFiles.length < 3">
|
||
|
|
<PlusOutlined />
|
||
|
|
<div class="ant-upload-text">Agregar Imagen</div>
|
||
|
|
<div class="ant-upload-hint">Máx. 3 imágenes, 2MB c/u</div>
|
||
|
|
</div>
|
||
|
|
</a-upload>
|
||
|
|
|
||
|
|
<!-- Vista previa de imágenes existentes (solo en edición) -->
|
||
|
|
<div v-if="isEditingPregunta && formPreguntaState.imagenes_explicacion_existentes?.length" class="existing-images">
|
||
|
|
<h4>Imágenes existentes:</h4>
|
||
|
|
<div class="image-grid">
|
||
|
|
<div v-for="(imagen, index) in formPreguntaState.imagenes_explicacion_existentes"
|
||
|
|
:key="'exp-existente-'+index"
|
||
|
|
class="image-item">
|
||
|
|
<img :src="getImageUrl(imagen)" :alt="'Imagen ' + (index+1)" />
|
||
|
|
<a-button
|
||
|
|
type="link"
|
||
|
|
danger
|
||
|
|
@click="eliminarImagenExistente('explicacion', index)"
|
||
|
|
class="remove-image-btn"
|
||
|
|
>
|
||
|
|
<DeleteOutlined />
|
||
|
|
</a-button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</a-form-item>
|
||
|
|
</a-card>
|
||
|
|
|
||
|
|
<!-- Configuración -->
|
||
|
|
<a-card size="small" class="section-card">
|
||
|
|
<template #title>
|
||
|
|
<div class="section-title">
|
||
|
|
<span>Configuración</span>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<div class="config-row">
|
||
|
|
<a-form-item
|
||
|
|
label="Nivel de Dificultad *"
|
||
|
|
name="nivel_dificultad"
|
||
|
|
:validate-status="getFieldStatus('nivel_dificultad')"
|
||
|
|
:help="getFieldHelp('nivel_dificultad')"
|
||
|
|
class="config-item"
|
||
|
|
>
|
||
|
|
<a-select
|
||
|
|
v-model:value="formPreguntaState.nivel_dificultad"
|
||
|
|
placeholder="Selecciona la dificultad"
|
||
|
|
>
|
||
|
|
<a-select-option value="facil">
|
||
|
|
<div class="dificultad-option">
|
||
|
|
<CheckCircleOutlined style="color: #52c41a;" />
|
||
|
|
<span>Fácil</span>
|
||
|
|
</div>
|
||
|
|
</a-select-option>
|
||
|
|
<a-select-option value="medio">
|
||
|
|
<div class="dificultad-option">
|
||
|
|
<WarningOutlined style="color: #fa8c16;" />
|
||
|
|
<span>Media</span>
|
||
|
|
</div>
|
||
|
|
</a-select-option>
|
||
|
|
<a-select-option value="dificil">
|
||
|
|
<div class="dificultad-option">
|
||
|
|
<CloseCircleOutlined style="color: #ff4d4f;" />
|
||
|
|
<span>Difícil</span>
|
||
|
|
</div>
|
||
|
|
</a-select-option>
|
||
|
|
</a-select>
|
||
|
|
</a-form-item>
|
||
|
|
|
||
|
|
<a-form-item
|
||
|
|
label="Estado"
|
||
|
|
class="config-item"
|
||
|
|
>
|
||
|
|
<a-switch
|
||
|
|
v-model:checked="formPreguntaState.activo"
|
||
|
|
:checked-children="'Activo'"
|
||
|
|
:un-checked-children="'Inactivo'"
|
||
|
|
/>
|
||
|
|
</a-form-item>
|
||
|
|
</div>
|
||
|
|
</a-card>
|
||
|
|
|
||
|
|
<!-- Errores -->
|
||
|
|
<div class="form-footer" v-if="preguntaStore.errors">
|
||
|
|
<a-alert
|
||
|
|
v-for="(errorList, field) in preguntaStore.errors"
|
||
|
|
:key="field"
|
||
|
|
type="error"
|
||
|
|
:message="field === 'message' ? errorList : `${field}: ${errorList[0]}`"
|
||
|
|
show-icon
|
||
|
|
class="error-alert"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</a-form>
|
||
|
|
</a-modal>
|
||
|
|
|
||
|
|
<!-- Modal de Vista de Pregunta -->
|
||
|
|
<a-modal
|
||
|
|
v-model:open="verPreguntaModalVisible"
|
||
|
|
:title="'Vista de Pregunta'"
|
||
|
|
width="800px"
|
||
|
|
:footer="null"
|
||
|
|
@cancel="verPreguntaModalVisible = false"
|
||
|
|
>
|
||
|
|
<div class="pregunta-view" v-if="preguntaSeleccionada">
|
||
|
|
<!-- Enunciado -->
|
||
|
|
<div class="enunciado-completo" v-html="preguntaSeleccionada.enunciado" />
|
||
|
|
|
||
|
|
<!-- Imágenes del enunciado -->
|
||
|
|
<div v-if="preguntaSeleccionada.imagenes && preguntaSeleccionada.imagenes.length"
|
||
|
|
class="imagenes-preview">
|
||
|
|
<h4>Imágenes del Enunciado:</h4>
|
||
|
|
<div class="imagenes-grid">
|
||
|
|
<img
|
||
|
|
v-for="(imagen, index) in preguntaSeleccionada.imagenes"
|
||
|
|
:key="index"
|
||
|
|
:src="getImageUrl(imagen)"
|
||
|
|
:alt="'Imagen ' + (index+1)"
|
||
|
|
@click="verImagen(getImageUrl(imagen))"
|
||
|
|
class="clickable-image"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Enunciado Adicional -->
|
||
|
|
<div v-if="preguntaSeleccionada.enunciado_adicional" class="enunciado-adicional">
|
||
|
|
<h4>Información Adicional:</h4>
|
||
|
|
<div v-html="preguntaSeleccionada.enunciado_adicional" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Opciones -->
|
||
|
|
<div class="opciones-view">
|
||
|
|
<h4>Opciones de Respuesta:</h4>
|
||
|
|
<div
|
||
|
|
v-for="(opcion, index) in (preguntaSeleccionada.opciones || [])"
|
||
|
|
:key="index"
|
||
|
|
class="opcion-view"
|
||
|
|
:class="{ correcta: opcion === preguntaSeleccionada.respuesta_correcta }"
|
||
|
|
>
|
||
|
|
<div class="opcion-letter">{{ String.fromCharCode(65 + index) }}.</div>
|
||
|
|
<div class="opcion-text">{{ opcion }}</div>
|
||
|
|
<div class="opcion-correct" v-if="opcion === preguntaSeleccionada.respuesta_correcta">
|
||
|
|
<CheckCircleOutlined style="color: #52c41a; margin-left: 8px;" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Explicación -->
|
||
|
|
<div v-if="preguntaSeleccionada.explicacion" class="explicacion-view">
|
||
|
|
<h4>Explicación:</h4>
|
||
|
|
<div v-html="preguntaSeleccionada.explicacion" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Imágenes de la explicación -->
|
||
|
|
<div v-if="preguntaSeleccionada.imagenes_explicacion && preguntaSeleccionada.imagenes_explicacion.length"
|
||
|
|
class="imagenes-preview">
|
||
|
|
<h4>Imágenes de la Explicación:</h4>
|
||
|
|
<div class="imagenes-grid">
|
||
|
|
<img
|
||
|
|
v-for="(imagen, index) in preguntaSeleccionada.imagenes_explicacion"
|
||
|
|
:key="index"
|
||
|
|
:src="getImageUrl(imagen)"
|
||
|
|
:alt="'Imagen ' + (index+1)"
|
||
|
|
@click="verImagen(getImageUrl(imagen))"
|
||
|
|
class="clickable-image"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Información Adicional -->
|
||
|
|
<div class="info-adicional">
|
||
|
|
<a-divider />
|
||
|
|
<div class="info-row">
|
||
|
|
<div class="info-item">
|
||
|
|
<strong>Dificultad:</strong>
|
||
|
|
<a-tag :color="getDificultadColor(preguntaSeleccionada.nivel_dificultad)">
|
||
|
|
{{ formatDificultad(preguntaSeleccionada.nivel_dificultad) }}
|
||
|
|
</a-tag>
|
||
|
|
</div>
|
||
|
|
<div class="info-item">
|
||
|
|
<strong>Estado:</strong>
|
||
|
|
<a-tag :color="preguntaSeleccionada.activo ? 'green' : 'red'">
|
||
|
|
{{ preguntaSeleccionada.activo ? 'Activo' : 'Inactivo' }}
|
||
|
|
</a-tag>
|
||
|
|
</div>
|
||
|
|
<div class="info-item">
|
||
|
|
<strong>Creado:</strong> {{ formatDate(preguntaSeleccionada.created_at) }}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</a-modal>
|
||
|
|
|
||
|
|
<!-- Modal de Confirmación para Eliminar Pregunta -->
|
||
|
|
<a-modal
|
||
|
|
v-model:open="deletePreguntaModalVisible"
|
||
|
|
title="Eliminar Pregunta"
|
||
|
|
@ok="handleDeletePregunta"
|
||
|
|
@cancel="deletePreguntaModalVisible = false"
|
||
|
|
ok-text="Eliminar"
|
||
|
|
cancel-text="Cancelar"
|
||
|
|
ok-type="danger"
|
||
|
|
>
|
||
|
|
<div class="delete-confirm-content">
|
||
|
|
<a-alert
|
||
|
|
message="¿Estás seguro de eliminar esta pregunta?"
|
||
|
|
description="Esta acción no se puede deshacer."
|
||
|
|
type="warning"
|
||
|
|
show-icon
|
||
|
|
/>
|
||
|
|
<div class="pregunta-info" v-if="preguntaToDelete">
|
||
|
|
<p><strong>Pregunta:</strong></p>
|
||
|
|
<div class="enunciado-preview" v-html="getEnunciadoPreview(preguntaToDelete.enunciado)" />
|
||
|
|
<p><strong>Dificultad:</strong> {{ formatDificultad(preguntaToDelete.nivel_dificultad) }}</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</a-modal>
|
||
|
|
|
||
|
|
<!-- Modal de Vista Previa de Imagen -->
|
||
|
|
<a-modal :open="previewVisible" :title="previewTitle" :footer="null" @cancel="previewVisible = false">
|
||
|
|
<img alt="Vista previa" style="width: 100%" :src="previewImage" />
|
||
|
|
</a-modal>
|
||
|
|
|
||
|
|
<!-- Modal para ver imagen en grande -->
|
||
|
|
<a-modal v-model:open="modalImagenVisible" :footer="null" @cancel="modalImagenVisible = false">
|
||
|
|
<img :src="imagenGrande" style="width: 100%" />
|
||
|
|
</a-modal>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup>
|
||
|
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||
|
|
import { useRoute, useRouter } from 'vue-router'
|
||
|
|
import { usePreguntaStore } from '../../../store/pregunta.store'
|
||
|
|
import { useCursoStore } from '../../../store/curso.store'
|
||
|
|
import { message } from 'ant-design-vue'
|
||
|
|
import {
|
||
|
|
PlusOutlined,
|
||
|
|
EditOutlined,
|
||
|
|
DeleteOutlined,
|
||
|
|
EyeOutlined,
|
||
|
|
QuestionCircleOutlined,
|
||
|
|
CheckCircleOutlined,
|
||
|
|
WarningOutlined,
|
||
|
|
CloseCircleOutlined,
|
||
|
|
ArrowLeftOutlined,
|
||
|
|
ReloadOutlined
|
||
|
|
} from '@ant-design/icons-vue'
|
||
|
|
|
||
|
|
// Router y Store
|
||
|
|
const route = useRoute()
|
||
|
|
const router = useRouter()
|
||
|
|
const preguntaStore = usePreguntaStore()
|
||
|
|
const cursoStore = useCursoStore()
|
||
|
|
|
||
|
|
// Estado
|
||
|
|
const modalPreguntaVisible = ref(false)
|
||
|
|
const verPreguntaModalVisible = ref(false)
|
||
|
|
const deletePreguntaModalVisible = ref(false)
|
||
|
|
const isEditingPregunta = ref(false)
|
||
|
|
const searchText = ref('')
|
||
|
|
const dificultadFilter = ref('')
|
||
|
|
const estadoFilter = ref('')
|
||
|
|
const preguntaSeleccionada = ref(null)
|
||
|
|
const preguntaToDelete = ref(null)
|
||
|
|
const formPreguntaRef = ref()
|
||
|
|
const modalImagenVisible = ref(false)
|
||
|
|
const imagenGrande = ref('')
|
||
|
|
|
||
|
|
// Estado para imágenes
|
||
|
|
const imagenesEnunciadoFiles = ref([])
|
||
|
|
const imagenesExplicacionFiles = ref([])
|
||
|
|
const previewVisible = ref(false)
|
||
|
|
const previewImage = ref('')
|
||
|
|
const previewTitle = ref('')
|
||
|
|
|
||
|
|
// Datos del curso
|
||
|
|
const curso = reactive({
|
||
|
|
id: null,
|
||
|
|
nombre: '',
|
||
|
|
codigo: '',
|
||
|
|
activo: true,
|
||
|
|
preguntas_count: 0
|
||
|
|
})
|
||
|
|
|
||
|
|
const estadisticas = reactive({
|
||
|
|
total: 0,
|
||
|
|
facil: 0,
|
||
|
|
medio: 0,
|
||
|
|
dificil: 0
|
||
|
|
})
|
||
|
|
|
||
|
|
const pagination = reactive({
|
||
|
|
current: 1,
|
||
|
|
pageSize: 10,
|
||
|
|
total: 0,
|
||
|
|
showSizeChanger: true,
|
||
|
|
pageSizeOptions: ['10', '20', '30', '50'],
|
||
|
|
showTotal: (total, range) => `${range[0]}-${range[1]} de ${total} preguntas`
|
||
|
|
})
|
||
|
|
|
||
|
|
// Formulario de Pregunta
|
||
|
|
const formPreguntaState = reactive({
|
||
|
|
id: null,
|
||
|
|
curso_id: null,
|
||
|
|
enunciado: '',
|
||
|
|
enunciado_adicional: '',
|
||
|
|
opciones: ['', ''],
|
||
|
|
respuesta_correcta: '',
|
||
|
|
explicacion: '',
|
||
|
|
nivel_dificultad: 'medio',
|
||
|
|
activo: true,
|
||
|
|
imagenes_existentes: [],
|
||
|
|
imagenes_explicacion_existentes: []
|
||
|
|
})
|
||
|
|
|
||
|
|
// Funciones de validación manual
|
||
|
|
const validarFormulario = () => {
|
||
|
|
const errors = []
|
||
|
|
|
||
|
|
// Validar opciones
|
||
|
|
const opcionesValidas = formPreguntaState.opciones.filter(op => op && op.trim() !== '')
|
||
|
|
if (opcionesValidas.length < 2) {
|
||
|
|
errors.push('Debe completar al menos 2 opciones')
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verificar duplicados
|
||
|
|
const opcionesSet = new Set(opcionesValidas.map(op => op.trim().toLowerCase()))
|
||
|
|
if (opcionesSet.size !== opcionesValidas.length) {
|
||
|
|
errors.push('No puede haber opciones duplicadas')
|
||
|
|
}
|
||
|
|
|
||
|
|
// Validar respuesta correcta
|
||
|
|
if (!formPreguntaState.respuesta_correcta || formPreguntaState.respuesta_correcta.trim() === '') {
|
||
|
|
errors.push('Debe seleccionar una respuesta correcta')
|
||
|
|
} else if (!opcionesValidas.includes(formPreguntaState.respuesta_correcta)) {
|
||
|
|
errors.push('La respuesta correcta debe estar entre las opciones')
|
||
|
|
}
|
||
|
|
|
||
|
|
return errors
|
||
|
|
}
|
||
|
|
|
||
|
|
// Reglas de validación (solo campos básicos, opciones se validan manualmente)
|
||
|
|
const formPreguntaRules = {
|
||
|
|
enunciado: [
|
||
|
|
{ required: true, message: 'El enunciado es requerido', trigger: 'blur' },
|
||
|
|
{ min: 5, message: 'El enunciado debe tener al menos 5 caracteres', trigger: 'blur' }
|
||
|
|
],
|
||
|
|
nivel_dificultad: [
|
||
|
|
{ required: true, message: 'La dificultad es requerida', trigger: 'change' }
|
||
|
|
]
|
||
|
|
}
|
||
|
|
|
||
|
|
// Columnas de la tabla de preguntas
|
||
|
|
const columns = [
|
||
|
|
{
|
||
|
|
title: 'ID',
|
||
|
|
dataIndex: 'id',
|
||
|
|
key: 'id',
|
||
|
|
width: 80
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Enunciado',
|
||
|
|
dataIndex: 'enunciado',
|
||
|
|
key: 'enunciado',
|
||
|
|
ellipsis: true
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Dificultad',
|
||
|
|
dataIndex: 'nivel_dificultad',
|
||
|
|
key: 'nivel_dificultad',
|
||
|
|
width: 120
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Estado',
|
||
|
|
dataIndex: 'activo',
|
||
|
|
key: 'activo',
|
||
|
|
width: 100
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Creado',
|
||
|
|
dataIndex: 'created_at',
|
||
|
|
key: 'created_at',
|
||
|
|
width: 150
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Acciones',
|
||
|
|
key: 'acciones',
|
||
|
|
width: 200,
|
||
|
|
align: 'center'
|
||
|
|
}
|
||
|
|
]
|
||
|
|
|
||
|
|
// Computed
|
||
|
|
const preguntas = computed(() => preguntaStore.preguntas)
|
||
|
|
|
||
|
|
// Métodos para manejar imágenes
|
||
|
|
const beforeUpload = (file) => {
|
||
|
|
const isImage = file.type.startsWith('image/')
|
||
|
|
const isLt2M = file.size / 1024 / 1024 < 2
|
||
|
|
|
||
|
|
if (!isImage) {
|
||
|
|
message.error('Solo se pueden subir imágenes!')
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!isLt2M) {
|
||
|
|
message.error('La imagen debe ser menor a 2MB!')
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
return false // Devolver false para manejar la subida manualmente
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleRemoveImagenEnunciado = (file) => {
|
||
|
|
const index = imagenesEnunciadoFiles.value.findIndex(f => f.uid === file.uid)
|
||
|
|
if (index !== -1) {
|
||
|
|
imagenesEnunciadoFiles.value.splice(index, 1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleRemoveImagenExplicacion = (file) => {
|
||
|
|
const index = imagenesExplicacionFiles.value.findIndex(f => f.uid === file.uid)
|
||
|
|
if (index !== -1) {
|
||
|
|
imagenesExplicacionFiles.value.splice(index, 1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handlePreview = async (file) => {
|
||
|
|
if (!file.url && !file.preview) {
|
||
|
|
file.preview = await getBase64(file.originFileObj)
|
||
|
|
}
|
||
|
|
previewImage.value = file.url || file.preview
|
||
|
|
previewVisible.value = true
|
||
|
|
previewTitle.value = file.name || file.url?.substring(file.url?.lastIndexOf('/') + 1) || 'Imagen'
|
||
|
|
}
|
||
|
|
|
||
|
|
const getBase64 = (file) => {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const reader = new FileReader()
|
||
|
|
reader.readAsDataURL(file)
|
||
|
|
reader.onload = () => resolve(reader.result)
|
||
|
|
reader.onerror = error => reject(error)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
const getImageUrl = (path) => {
|
||
|
|
if (!path) return ''
|
||
|
|
if (path.startsWith('http')) return path
|
||
|
|
return `/storage/${path}`
|
||
|
|
}
|
||
|
|
|
||
|
|
const eliminarImagenExistente = (tipo, index) => {
|
||
|
|
if (tipo === 'enunciado') {
|
||
|
|
formPreguntaState.imagenes_existentes.splice(index, 1)
|
||
|
|
message.success('Imagen eliminada (se aplicará al guardar)')
|
||
|
|
} else if (tipo === 'explicacion') {
|
||
|
|
formPreguntaState.imagenes_explicacion_existentes.splice(index, 1)
|
||
|
|
message.success('Imagen eliminada (se aplicará al guardar)')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Métodos para ver imágenes en grande
|
||
|
|
const verImagen = (url) => {
|
||
|
|
imagenGrande.value = url
|
||
|
|
modalImagenVisible.value = true
|
||
|
|
}
|
||
|
|
|
||
|
|
// Métodos principales
|
||
|
|
const goBack = () => {
|
||
|
|
router.push({ name: 'AcademiaCursos' })
|
||
|
|
}
|
||
|
|
|
||
|
|
const fetchPreguntas = async () => {
|
||
|
|
const params = {
|
||
|
|
page: pagination.current,
|
||
|
|
per_page: pagination.pageSize
|
||
|
|
}
|
||
|
|
|
||
|
|
if (searchText.value) params.search = searchText.value
|
||
|
|
if (dificultadFilter.value) params.nivel_dificultad = dificultadFilter.value
|
||
|
|
if (estadoFilter.value !== '') params.activo = estadoFilter.value === 'true'
|
||
|
|
|
||
|
|
const data = await preguntaStore.fetchPreguntasByCurso(curso.id, params)
|
||
|
|
|
||
|
|
if (data) {
|
||
|
|
// Actualizar estadísticas
|
||
|
|
estadisticas.total = data.estadisticas?.total || 0
|
||
|
|
estadisticas.facil = data.estadisticas?.facil || 0
|
||
|
|
estadisticas.medio = data.estadisticas?.medio || 0
|
||
|
|
estadisticas.dificil = data.estadisticas?.dificil || 0
|
||
|
|
|
||
|
|
// Actualizar paginación
|
||
|
|
if (data.preguntas) {
|
||
|
|
pagination.total = data.preguntas.total || 0
|
||
|
|
pagination.current = data.preguntas.current_page || 1
|
||
|
|
pagination.pageSize = data.preguntas.per_page || 10
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const loadCursoInfo = async () => {
|
||
|
|
const cursoId = parseInt(route.params.id)
|
||
|
|
curso.id = cursoId
|
||
|
|
|
||
|
|
const cursoEncontrado = cursoStore.cursos.find(c => c.id === cursoId)
|
||
|
|
|
||
|
|
if (cursoEncontrado) {
|
||
|
|
Object.assign(curso, cursoEncontrado)
|
||
|
|
} else {
|
||
|
|
curso.nombre = `Curso ${cursoId}`
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const showCreateModal = () => {
|
||
|
|
isEditingPregunta.value = false
|
||
|
|
resetPreguntaForm()
|
||
|
|
formPreguntaState.curso_id = curso.id
|
||
|
|
modalPreguntaVisible.value = true
|
||
|
|
preguntaStore.errors = null
|
||
|
|
}
|
||
|
|
|
||
|
|
const showEditModal = (pregunta) => {
|
||
|
|
isEditingPregunta.value = true
|
||
|
|
resetPreguntaForm()
|
||
|
|
|
||
|
|
// Llenar formulario con datos de la pregunta
|
||
|
|
formPreguntaState.id = pregunta.id
|
||
|
|
formPreguntaState.curso_id = curso.id
|
||
|
|
formPreguntaState.enunciado = pregunta.enunciado
|
||
|
|
formPreguntaState.enunciado_adicional = pregunta.enunciado_adicional || ''
|
||
|
|
formPreguntaState.opciones = pregunta.opciones && Array.isArray(pregunta.opciones) ? pregunta.opciones : ['']
|
||
|
|
formPreguntaState.respuesta_correcta = pregunta.respuesta_correcta || ''
|
||
|
|
formPreguntaState.explicacion = pregunta.explicacion || ''
|
||
|
|
formPreguntaState.nivel_dificultad = pregunta.nivel_dificultad
|
||
|
|
formPreguntaState.activo = pregunta.activo
|
||
|
|
|
||
|
|
// Cargar imágenes existentes
|
||
|
|
formPreguntaState.imagenes_existentes = pregunta.imagenes || []
|
||
|
|
formPreguntaState.imagenes_explicacion_existentes = pregunta.imagenes_explicacion || []
|
||
|
|
|
||
|
|
// Limpiar archivos nuevos
|
||
|
|
imagenesEnunciadoFiles.value = []
|
||
|
|
imagenesExplicacionFiles.value = []
|
||
|
|
|
||
|
|
modalPreguntaVisible.value = true
|
||
|
|
preguntaStore.errors = null
|
||
|
|
}
|
||
|
|
|
||
|
|
const verPregunta = (pregunta) => {
|
||
|
|
preguntaSeleccionada.value = pregunta
|
||
|
|
verPreguntaModalVisible.value = true
|
||
|
|
}
|
||
|
|
|
||
|
|
const confirmDeletePregunta = (pregunta) => {
|
||
|
|
preguntaToDelete.value = pregunta
|
||
|
|
deletePreguntaModalVisible.value = true
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleDeletePregunta = async () => {
|
||
|
|
try {
|
||
|
|
await preguntaStore.eliminarPregunta(preguntaToDelete.value.id)
|
||
|
|
message.success('Pregunta eliminada correctamente')
|
||
|
|
deletePreguntaModalVisible.value = false
|
||
|
|
preguntaToDelete.value = null
|
||
|
|
await fetchPreguntas()
|
||
|
|
} catch (error) {
|
||
|
|
message.error('Error al eliminar la pregunta')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleModalPreguntaOk = async () => {
|
||
|
|
try {
|
||
|
|
// Validar campos básicos con Ant Design
|
||
|
|
await formPreguntaRef.value.validateFields()
|
||
|
|
|
||
|
|
// Luego validar opciones y respuesta correcta manualmente
|
||
|
|
const erroresValidacion = validarFormulario()
|
||
|
|
if (erroresValidacion.length > 0) {
|
||
|
|
erroresValidacion.forEach(error => {
|
||
|
|
message.error(error)
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Si pasa todas las validaciones, proceder
|
||
|
|
await submitPreguntaForm()
|
||
|
|
} catch (error) {
|
||
|
|
console.log('Validación fallida:', error)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleModalPreguntaCancel = () => {
|
||
|
|
modalPreguntaVisible.value = false
|
||
|
|
resetPreguntaForm()
|
||
|
|
preguntaStore.errors = null
|
||
|
|
}
|
||
|
|
|
||
|
|
const submitPreguntaForm = async () => {
|
||
|
|
try {
|
||
|
|
// Validar antes de enviar
|
||
|
|
const erroresValidacion = validarFormulario()
|
||
|
|
if (erroresValidacion.length > 0) {
|
||
|
|
erroresValidacion.forEach(error => {
|
||
|
|
message.error(error)
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Crear FormData para enviar archivos
|
||
|
|
const formData = new FormData()
|
||
|
|
|
||
|
|
// Agregar campos básicos
|
||
|
|
formData.append('curso_id', formPreguntaState.curso_id)
|
||
|
|
formData.append('enunciado', formPreguntaState.enunciado)
|
||
|
|
formData.append('nivel_dificultad', formPreguntaState.nivel_dificultad)
|
||
|
|
formData.append('activo', formPreguntaState.activo ? 1 : 0)
|
||
|
|
|
||
|
|
if (formPreguntaState.enunciado_adicional) {
|
||
|
|
formData.append('enunciado_adicional', formPreguntaState.enunciado_adicional)
|
||
|
|
}
|
||
|
|
|
||
|
|
if (formPreguntaState.explicacion) {
|
||
|
|
formData.append('explicacion', formPreguntaState.explicacion)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Agregar opciones como JSON
|
||
|
|
const opcionesValidas = formPreguntaState.opciones.filter(op => op && op.trim() !== '')
|
||
|
|
formData.append('opciones', JSON.stringify(opcionesValidas))
|
||
|
|
|
||
|
|
if (formPreguntaState.respuesta_correcta) {
|
||
|
|
formData.append('respuesta_correcta', formPreguntaState.respuesta_correcta)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Agregar nuevas imágenes del enunciado
|
||
|
|
imagenesEnunciadoFiles.value.forEach(file => {
|
||
|
|
if (file.originFileObj) {
|
||
|
|
formData.append('imagenes[]', file.originFileObj)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
// Agregar nuevas imágenes de la explicación
|
||
|
|
imagenesExplicacionFiles.value.forEach(file => {
|
||
|
|
if (file.originFileObj) {
|
||
|
|
formData.append('imagenes_explicacion[]', file.originFileObj)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
if (isEditingPregunta.value) {
|
||
|
|
// Para edición, también enviar imágenes existentes que no fueron eliminadas
|
||
|
|
formData.append('imagenes_existentes', JSON.stringify(formPreguntaState.imagenes_existentes))
|
||
|
|
formData.append('imagenes_explicacion_existentes', JSON.stringify(formPreguntaState.imagenes_explicacion_existentes))
|
||
|
|
|
||
|
|
await preguntaStore.actualizarPregunta(formPreguntaState.id, formData)
|
||
|
|
message.success('Pregunta actualizada correctamente')
|
||
|
|
} else {
|
||
|
|
await preguntaStore.crearPregunta(formData)
|
||
|
|
message.success('Pregunta creada correctamente')
|
||
|
|
}
|
||
|
|
|
||
|
|
modalPreguntaVisible.value = false
|
||
|
|
resetPreguntaForm()
|
||
|
|
preguntaStore.errors = null
|
||
|
|
await fetchPreguntas()
|
||
|
|
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error al guardar pregunta:', error)
|
||
|
|
message.error('Error al guardar la pregunta')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const togglePreguntaStatus = async (pregunta) => {
|
||
|
|
try {
|
||
|
|
const payload = {
|
||
|
|
curso_id: curso.id,
|
||
|
|
enunciado: pregunta.enunciado,
|
||
|
|
enunciado_adicional: pregunta.enunciado_adicional,
|
||
|
|
opciones: pregunta.opciones,
|
||
|
|
respuesta_correcta: pregunta.respuesta_correcta,
|
||
|
|
explicacion: pregunta.explicacion,
|
||
|
|
nivel_dificultad: pregunta.nivel_dificultad,
|
||
|
|
activo: !pregunta.activo
|
||
|
|
}
|
||
|
|
|
||
|
|
await preguntaStore.actualizarPregunta(pregunta.id, payload)
|
||
|
|
message.success(`Pregunta ${pregunta.activo ? 'desactivada' : 'activada'} correctamente`)
|
||
|
|
await fetchPreguntas()
|
||
|
|
} catch (error) {
|
||
|
|
message.error('Error al cambiar el estado de la pregunta')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const resetPreguntaForm = () => {
|
||
|
|
formPreguntaState.id = null
|
||
|
|
formPreguntaState.curso_id = null
|
||
|
|
formPreguntaState.enunciado = ''
|
||
|
|
formPreguntaState.enunciado_adicional = ''
|
||
|
|
formPreguntaState.opciones = ['', '']
|
||
|
|
formPreguntaState.respuesta_correcta = ''
|
||
|
|
formPreguntaState.explicacion = ''
|
||
|
|
formPreguntaState.nivel_dificultad = 'medio'
|
||
|
|
formPreguntaState.activo = true
|
||
|
|
formPreguntaState.imagenes_existentes = []
|
||
|
|
formPreguntaState.imagenes_explicacion_existentes = []
|
||
|
|
|
||
|
|
imagenesEnunciadoFiles.value = []
|
||
|
|
imagenesExplicacionFiles.value = []
|
||
|
|
|
||
|
|
if (formPreguntaRef.value) {
|
||
|
|
formPreguntaRef.value.resetFields()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const addOpcion = () => {
|
||
|
|
if (formPreguntaState.opciones.length < 5) {
|
||
|
|
formPreguntaState.opciones.push('')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const removeOpcion = (index) => {
|
||
|
|
if (formPreguntaState.opciones.length > 2) {
|
||
|
|
const opcionEliminada = formPreguntaState.opciones[index]
|
||
|
|
formPreguntaState.opciones.splice(index, 1)
|
||
|
|
// Si eliminamos la opción correcta, limpiar respuesta_correcta
|
||
|
|
if (opcionEliminada === formPreguntaState.respuesta_correcta) {
|
||
|
|
formPreguntaState.respuesta_correcta = ''
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const updateOpcion = (index, value) => {
|
||
|
|
formPreguntaState.opciones[index] = value
|
||
|
|
}
|
||
|
|
|
||
|
|
const setRespuestaCorrecta = (opcion) => {
|
||
|
|
formPreguntaState.respuesta_correcta = opcion
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleSearch = () => {
|
||
|
|
pagination.current = 1
|
||
|
|
fetchPreguntas()
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleFilterChange = () => {
|
||
|
|
pagination.current = 1
|
||
|
|
fetchPreguntas()
|
||
|
|
}
|
||
|
|
|
||
|
|
const clearFilters = () => {
|
||
|
|
searchText.value = ''
|
||
|
|
dificultadFilter.value = ''
|
||
|
|
estadoFilter.value = ''
|
||
|
|
pagination.current = 1
|
||
|
|
fetchPreguntas()
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleTableChange = (paginationConfig) => {
|
||
|
|
pagination.current = paginationConfig.current
|
||
|
|
pagination.pageSize = paginationConfig.pageSize
|
||
|
|
fetchPreguntas()
|
||
|
|
}
|
||
|
|
|
||
|
|
const formatDate = (dateString) => {
|
||
|
|
if (!dateString) return ''
|
||
|
|
const date = new Date(dateString)
|
||
|
|
return date.toLocaleDateString('es-ES', {
|
||
|
|
day: '2-digit',
|
||
|
|
month: '2-digit',
|
||
|
|
year: 'numeric'
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
const formatDificultad = (dificultad) => {
|
||
|
|
const map = {
|
||
|
|
'facil': 'Fácil',
|
||
|
|
'medio': 'Media',
|
||
|
|
'dificil': 'Difícil'
|
||
|
|
}
|
||
|
|
return map[dificultad] || dificultad
|
||
|
|
}
|
||
|
|
|
||
|
|
const getDificultadColor = (dificultad) => {
|
||
|
|
const map = {
|
||
|
|
'facil': 'green',
|
||
|
|
'medio': 'orange',
|
||
|
|
'dificil': 'red'
|
||
|
|
}
|
||
|
|
return map[dificultad] || 'default'
|
||
|
|
}
|
||
|
|
|
||
|
|
const getEnunciadoPreview = (enunciado) => {
|
||
|
|
if (!enunciado) return ''
|
||
|
|
// Remover HTML tags y limitar a 100 caracteres
|
||
|
|
const text = enunciado.replace(/<[^>]*>/g, '')
|
||
|
|
return text.length > 100 ? text.substring(0, 100) + '...' : text
|
||
|
|
}
|
||
|
|
|
||
|
|
const getFieldStatus = (fieldName) => {
|
||
|
|
if (preguntaStore.errors && preguntaStore.errors[fieldName]) {
|
||
|
|
return 'error'
|
||
|
|
}
|
||
|
|
return ''
|
||
|
|
}
|
||
|
|
|
||
|
|
const getFieldHelp = (fieldName) => {
|
||
|
|
if (preguntaStore.errors && preguntaStore.errors[fieldName]) {
|
||
|
|
return preguntaStore.errors[fieldName][0]
|
||
|
|
}
|
||
|
|
return ''
|
||
|
|
}
|
||
|
|
|
||
|
|
// Lifecycle
|
||
|
|
onMounted(async () => {
|
||
|
|
await loadCursoInfo()
|
||
|
|
await fetchPreguntas()
|
||
|
|
})
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
.preguntas-curso-view {
|
||
|
|
padding: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.view-header {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
margin-bottom: 24px;
|
||
|
|
padding: 0 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.header-left {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.back-btn {
|
||
|
|
padding: 0;
|
||
|
|
height: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.header-title h2 {
|
||
|
|
margin: 0;
|
||
|
|
font-size: 24px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #1f1f1f;
|
||
|
|
}
|
||
|
|
|
||
|
|
.curso-info {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 12px;
|
||
|
|
margin-top: 4px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.curso-codigo {
|
||
|
|
font-size: 14px;
|
||
|
|
color: #666;
|
||
|
|
background: #f5f5f5;
|
||
|
|
padding: 2px 8px;
|
||
|
|
border-radius: 4px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.curso-stats {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
|
|
gap: 16px;
|
||
|
|
margin-bottom: 24px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-card {
|
||
|
|
border-radius: 12px;
|
||
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||
|
|
border: 1px solid #f0f0f0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-content {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-icon-wrapper {
|
||
|
|
width: 48px;
|
||
|
|
height: 48px;
|
||
|
|
border-radius: 12px;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
font-size: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-info {
|
||
|
|
flex: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-value {
|
||
|
|
font-size: 24px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #1f1f1f;
|
||
|
|
line-height: 1.3;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-label {
|
||
|
|
font-size: 14px;
|
||
|
|
color: #666;
|
||
|
|
margin-top: 2px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.filters-section {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
margin-bottom: 24px;
|
||
|
|
padding: 0 8px;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.filters {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.filter-actions {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.preguntas-table-container {
|
||
|
|
background: white;
|
||
|
|
border-radius: 12px;
|
||
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||
|
|
border: 1px solid #f0f0f0;
|
||
|
|
overflow: hidden;
|
||
|
|
}
|
||
|
|
|
||
|
|
.preguntas-table {
|
||
|
|
border-radius: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.preguntas-table :deep(.ant-table) {
|
||
|
|
border-radius: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.preguntas-table :deep(.ant-table-thead > tr > th) {
|
||
|
|
background: #fafafa;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #1f1f1f;
|
||
|
|
border-bottom: 1px solid #f0f0f0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.preguntas-table :deep(.ant-table-tbody > tr > td) {
|
||
|
|
border-bottom: 1px solid #f0f0f0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.preguntas-table :deep(.ant-table-tbody > tr:hover > td) {
|
||
|
|
background: #fafafa;
|
||
|
|
}
|
||
|
|
|
||
|
|
.enunciado-preview {
|
||
|
|
max-height: 40px;
|
||
|
|
overflow: hidden;
|
||
|
|
text-overflow: ellipsis;
|
||
|
|
display: -webkit-box;
|
||
|
|
|
||
|
|
-webkit-box-orient: vertical;
|
||
|
|
}
|
||
|
|
|
||
|
|
.action-btn {
|
||
|
|
padding: 4px 8px;
|
||
|
|
height: auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.action-btn :deep(.anticon) {
|
||
|
|
font-size: 14px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.pregunta-modal :deep(.ant-modal-body) {
|
||
|
|
padding: 24px;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Estilos adicionales */
|
||
|
|
.section-card {
|
||
|
|
margin-bottom: 16px;
|
||
|
|
border-radius: 8px;
|
||
|
|
border: 1px solid #f0f0f0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.section-title {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.section-subtitle {
|
||
|
|
font-size: 12px;
|
||
|
|
color: #999;
|
||
|
|
font-weight: normal;
|
||
|
|
}
|
||
|
|
|
||
|
|
.upload-section {
|
||
|
|
margin-top: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.existing-images {
|
||
|
|
margin-top: 16px;
|
||
|
|
padding: 12px;
|
||
|
|
background: #fafafa;
|
||
|
|
border-radius: 8px;
|
||
|
|
border: 1px dashed #d9d9d9;
|
||
|
|
}
|
||
|
|
|
||
|
|
.existing-images h4 {
|
||
|
|
margin: 0 0 12px 0;
|
||
|
|
color: #666;
|
||
|
|
font-size: 14px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.image-grid {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||
|
|
gap: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.image-item {
|
||
|
|
position: relative;
|
||
|
|
border-radius: 8px;
|
||
|
|
overflow: hidden;
|
||
|
|
border: 1px solid #e8e8e8;
|
||
|
|
}
|
||
|
|
|
||
|
|
.image-item img {
|
||
|
|
width: 100%;
|
||
|
|
height: 100px;
|
||
|
|
object-fit: cover;
|
||
|
|
}
|
||
|
|
|
||
|
|
.remove-image-btn {
|
||
|
|
position: absolute;
|
||
|
|
top: 4px;
|
||
|
|
right: 4px;
|
||
|
|
background: rgba(0, 0, 0, 0.5);
|
||
|
|
color: white;
|
||
|
|
border: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.remove-image-btn:hover {
|
||
|
|
background: rgba(255, 0, 0, 0.7);
|
||
|
|
}
|
||
|
|
|
||
|
|
.opcion-item {
|
||
|
|
padding: 12px;
|
||
|
|
margin-bottom: 12px;
|
||
|
|
border: 1px solid #f0f0f0;
|
||
|
|
border-radius: 8px;
|
||
|
|
background: #fafafa;
|
||
|
|
}
|
||
|
|
|
||
|
|
.opcion-item.correct-option {
|
||
|
|
border-color: #52c41a;
|
||
|
|
background: #f6ffed;
|
||
|
|
}
|
||
|
|
|
||
|
|
.opcion-header {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: flex-start;
|
||
|
|
}
|
||
|
|
|
||
|
|
.opcion-label {
|
||
|
|
flex: 1;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.opcion-letter {
|
||
|
|
font-weight: bold;
|
||
|
|
color: #1890ff;
|
||
|
|
min-width: 24px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.opcion-input {
|
||
|
|
flex: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.opcion-actions {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.config-row {
|
||
|
|
display: flex;
|
||
|
|
gap: 24px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.config-item {
|
||
|
|
flex: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dificultad-option {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.form-footer {
|
||
|
|
margin-top: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.error-alert {
|
||
|
|
margin-bottom: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.error-alert:last-child {
|
||
|
|
margin-bottom: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Vista de Pregunta */
|
||
|
|
.pregunta-view {
|
||
|
|
max-height: 600px;
|
||
|
|
overflow-y: auto;
|
||
|
|
padding-right: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.pregunta-view::-webkit-scrollbar {
|
||
|
|
width: 6px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.pregunta-view::-webkit-scrollbar-track {
|
||
|
|
background: #f1f1f1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.pregunta-view::-webkit-scrollbar-thumb {
|
||
|
|
background: #888;
|
||
|
|
border-radius: 3px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.enunciado-completo {
|
||
|
|
font-size: 16px;
|
||
|
|
line-height: 1.6;
|
||
|
|
margin-bottom: 20px;
|
||
|
|
padding: 16px;
|
||
|
|
background: #fafafa;
|
||
|
|
border-radius: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.imagenes-preview {
|
||
|
|
margin: 16px 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.imagenes-preview h4 {
|
||
|
|
margin: 16px 0 8px 0;
|
||
|
|
color: #666;
|
||
|
|
}
|
||
|
|
|
||
|
|
.imagenes-grid {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: 12px;
|
||
|
|
margin-top: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.clickable-image {
|
||
|
|
max-width: 200px;
|
||
|
|
max-height: 200px;
|
||
|
|
border-radius: 8px;
|
||
|
|
border: 1px solid #f0f0f0;
|
||
|
|
cursor: pointer;
|
||
|
|
transition: transform 0.2s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.clickable-image:hover {
|
||
|
|
transform: scale(1.05);
|
||
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
|
|
}
|
||
|
|
|
||
|
|
.enunciado-adicional {
|
||
|
|
margin-bottom: 20px;
|
||
|
|
padding: 16px;
|
||
|
|
background: #f0f0f0;
|
||
|
|
border-radius: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.enunciado-adicional h4 {
|
||
|
|
margin-top: 0;
|
||
|
|
color: #666;
|
||
|
|
}
|
||
|
|
|
||
|
|
.opciones-view {
|
||
|
|
margin-bottom: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.opcion-view {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
padding: 12px 16px;
|
||
|
|
margin-bottom: 8px;
|
||
|
|
background: #fafafa;
|
||
|
|
border-radius: 8px;
|
||
|
|
border: 1px solid #f0f0f0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.opcion-view.correcta {
|
||
|
|
background: #f6ffed;
|
||
|
|
border-color: #b7eb8f;
|
||
|
|
}
|
||
|
|
|
||
|
|
.opcion-letter {
|
||
|
|
font-weight: bold;
|
||
|
|
margin-right: 12px;
|
||
|
|
min-width: 24px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.opcion-text {
|
||
|
|
flex: 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
.explicacion-view {
|
||
|
|
padding: 16px;
|
||
|
|
background: #e6f7ff;
|
||
|
|
border-radius: 8px;
|
||
|
|
margin-bottom: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.explicacion-view h4 {
|
||
|
|
margin-top: 0;
|
||
|
|
color: #1890ff;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-adicional {
|
||
|
|
margin-top: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-row {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: 24px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-item {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.delete-confirm-content {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.pregunta-info {
|
||
|
|
background: #fafafa;
|
||
|
|
padding: 12px;
|
||
|
|
border-radius: 8px;
|
||
|
|
border: 1px solid #f0f0f0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Ajustes para el upload de imágenes */
|
||
|
|
:deep(.ant-upload-select-picture-card) {
|
||
|
|
width: 100px;
|
||
|
|
height: 100px;
|
||
|
|
margin-right: 8px;
|
||
|
|
margin-bottom: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
:deep(.ant-upload-list-picture-card-container) {
|
||
|
|
width: 100px;
|
||
|
|
height: 100px;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Responsive */
|
||
|
|
@media (max-width: 768px) {
|
||
|
|
.view-header {
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: flex-start;
|
||
|
|
gap: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.header-right {
|
||
|
|
align-self: flex-start;
|
||
|
|
}
|
||
|
|
|
||
|
|
.filters-section {
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: stretch;
|
||
|
|
}
|
||
|
|
|
||
|
|
.filters {
|
||
|
|
flex-direction: column;
|
||
|
|
width: 100%;
|
||
|
|
}
|
||
|
|
|
||
|
|
.filters .ant-input-search,
|
||
|
|
.filters .ant-select {
|
||
|
|
width: 100%;
|
||
|
|
margin-left: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.curso-stats {
|
||
|
|
grid-template-columns: 1fr;
|
||
|
|
}
|
||
|
|
|
||
|
|
.config-row {
|
||
|
|
flex-direction: column;
|
||
|
|
}
|
||
|
|
|
||
|
|
.opcion-header {
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: flex-start;
|
||
|
|
gap: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.opcion-actions {
|
||
|
|
align-self: flex-end;
|
||
|
|
}
|
||
|
|
|
||
|
|
.imagenes-grid {
|
||
|
|
justify-content: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.clickable-image {
|
||
|
|
max-width: 150px;
|
||
|
|
max-height: 150px;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|