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.
849 lines
20 KiB
Vue
849 lines
20 KiB
Vue
<template>
|
|
<div class="exam-page">
|
|
<!-- HEADER PROFESIONAL -->
|
|
<a-card class="top-card" :bordered="false">
|
|
<div class="topbar">
|
|
<div class="top-left">
|
|
<div class="title">
|
|
<div class="sub">
|
|
<span class="area">{{ examenInfo.area || "Área no disponible" }}</span>
|
|
<span class="sep">•</span>
|
|
<span class="prog">Pregunta {{ indiceActual + 1 }} / {{ totalPreguntas }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="meta">
|
|
<span><b>Duración:</b> {{ examenInfo.duracion }} min</span>
|
|
<span class="sep">•</span>
|
|
<span><b>Intentos:</b> {{ examenInfo.intentos }} / {{ examenInfo.intentos_maximos }}</span>
|
|
</div>
|
|
|
|
<a-progress class="progress" :percent="porcentajeCompletado" :show-info="false" />
|
|
<div class="progressText">
|
|
Progreso: <b>{{ preguntasRespondidas }}</b> guardadas de <b>{{ totalPreguntas }}</b>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="top-right">
|
|
<div class="statusPill">
|
|
<span class="dot" :class="{ ok: porcentajeCompletado === 100 }"></span>
|
|
<span>{{ porcentajeCompletado === 100 ? "Listo para finalizar" : "En progreso" }}</span>
|
|
</div>
|
|
|
|
<div class="timerBox">
|
|
<div class="timerLabel">Tiempo restante</div>
|
|
<a-statistic-countdown
|
|
class="timer"
|
|
:value="timerValue"
|
|
@finish="finalizarExamenAutomaticamente"
|
|
format="HH:mm:ss"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</a-card>
|
|
|
|
<!-- LOADING -->
|
|
<a-card v-if="cargandoInicio" class="card" :bordered="false">
|
|
<a-skeleton active />
|
|
</a-card>
|
|
|
|
<!-- CONTENIDO -->
|
|
<a-card v-else-if="preguntaActual" class="card question-card" :bordered="false">
|
|
<!-- Cabecera pregunta -->
|
|
<div class="q-header">
|
|
<div class="q-left">
|
|
<div class="q-number">Pregunta {{ indiceActual + 1 }}</div>
|
|
|
|
<div class="q-tags">
|
|
<span v-if="preguntaActual.curso" class="chip">
|
|
{{ preguntaActual.curso }}
|
|
</span>
|
|
<span class="chip muted">
|
|
{{ estadoTexto(preguntaActual) }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="custom-divider"></div>
|
|
|
|
<!-- ENUNCIADO (SIN MARCOS: SOLO TEXTO + LÍNEAS) -->
|
|
<div class="qBody">
|
|
<div class="qStatement">
|
|
<MarkdownLatex :content="preguntaActual.enunciado" />
|
|
</div>
|
|
|
|
<div v-if="imagenesPregunta.length" class="imgWrap">
|
|
<a-image-preview-group>
|
|
<div class="imgGrid">
|
|
<a-image
|
|
v-for="(src, i) in imagenesPregunta"
|
|
:key="i"
|
|
:src="src"
|
|
:alt="`Imagen ${i + 1}`"
|
|
class="img"
|
|
/>
|
|
</div>
|
|
</a-image-preview-group>
|
|
</div>
|
|
|
|
<div v-if="preguntaActual.extra && preguntaActual.extra !== preguntaActual.enunciado" class="qExtra">
|
|
<MarkdownLatex :content="preguntaActual.extra" />
|
|
</div>
|
|
|
|
<!-- solo línea -->
|
|
<div class="lineDivider"></div>
|
|
</div>
|
|
|
|
<!-- RESPUESTAS (SIN MARCOS: SOLO LÍNEAS) -->
|
|
<div class="answer">
|
|
<div v-if="tieneOpciones(preguntaActual)">
|
|
<a-radio-group
|
|
v-model:value="preguntaActual.respuestaSeleccionada"
|
|
:disabled="preguntaActual.estado === 'respondida' || guardando || finalizando"
|
|
class="radio-group"
|
|
>
|
|
<div class="optList">
|
|
<a-radio
|
|
v-for="op in preguntaActual.opcionesOrdenadas"
|
|
:key="op.key"
|
|
:value="op.key.toString()"
|
|
class="optRow"
|
|
>
|
|
<span class="optKey">{{ getLetraOpcion(op.key) }})</span>
|
|
<span class="optTextInline">
|
|
<MarkdownLatex :content="op.texto" />
|
|
</span>
|
|
</a-radio>
|
|
</div>
|
|
</a-radio-group>
|
|
</div>
|
|
|
|
<!-- Abierta -->
|
|
<div v-else>
|
|
<a-textarea
|
|
v-model:value="preguntaActual.respuestaTexto"
|
|
:rows="6"
|
|
placeholder="Escribe tu respuesta..."
|
|
:disabled="preguntaActual.estado === 'respondida' || guardando || finalizando"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- NAV -->
|
|
<div class="nav">
|
|
<a-button :disabled="indiceActual === 0 || guardando || finalizando" @click="irAnterior" class="btn">
|
|
Atrás
|
|
</a-button>
|
|
|
|
<a-button
|
|
type="primary"
|
|
:loading="guardando || finalizando"
|
|
@click="siguienteAccion"
|
|
class="btn primary"
|
|
>
|
|
{{ esUltima ? "Guardar y finalizar" : "Siguiente" }}
|
|
</a-button>
|
|
</div>
|
|
|
|
<!-- Nota -->
|
|
<div class="note">
|
|
Al presionar <b>Siguiente</b>, tu respuesta se guarda. Evita recargar o cerrar durante el examen.
|
|
</div>
|
|
</a-card>
|
|
|
|
<!-- SIN PREGUNTAS -->
|
|
<a-card v-else class="card" :bordered="false">
|
|
<a-alert
|
|
type="warning"
|
|
show-icon
|
|
message="No hay preguntas para mostrar"
|
|
description="Verifica que el examen tenga preguntas generadas."
|
|
/>
|
|
<div class="nav" style="margin-top: 12px">
|
|
<a-button @click="router.push({ name: 'DashboardPostulante' })" class="btn">
|
|
Volver
|
|
</a-button>
|
|
</div>
|
|
</a-card>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted, onBeforeUnmount, watch, h } from "vue";
|
|
import { useRoute, useRouter } from "vue-router";
|
|
import { useExamenStore } from "../../store/examen.store";
|
|
import { message, Modal, Spin } from "ant-design-vue";
|
|
import MarkdownLatex from "../../views/administrador/cursos/MarkdownLatex.vue";
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const examenStore = useExamenStore();
|
|
|
|
const guardando = ref(false);
|
|
const finalizando = ref(false);
|
|
const timerValue = ref(null);
|
|
let timerIntervalId = null;
|
|
|
|
const preguntasLocal = ref([]);
|
|
const indiceActual = ref(0);
|
|
const cargandoInicio = ref(false);
|
|
const initOnce = ref(false);
|
|
|
|
/* INFO EXAMEN */
|
|
const examenInfo = computed(() => {
|
|
if (!examenStore.examenActual) {
|
|
return { proceso: null, area: null, duracion: 60, intentos: 0, intentos_maximos: 3 };
|
|
}
|
|
return {
|
|
proceso: examenStore.examenActual?.proceso || "Proceso no disponible",
|
|
area: examenStore.examenActual?.area || "Área no disponible",
|
|
duracion: examenStore.examenActual?.duracion || 60,
|
|
intentos: examenStore.examenActual?.intentos || 0,
|
|
intentos_maximos: examenStore.examenActual?.intentos_maximos || 3,
|
|
};
|
|
});
|
|
|
|
/* TRANSFORMAR */
|
|
const transformarPreguntas = (arr) => {
|
|
if (!Array.isArray(arr)) return [];
|
|
return arr.map((p) => ({
|
|
...p,
|
|
opcionesOrdenadas: p.opciones ? [...p.opciones].sort((a, b) => a.key - b.key) : [],
|
|
respuestaSeleccionada: p.respuestaSeleccionada ?? null,
|
|
respuestaTexto: p.respuestaTexto ?? "",
|
|
}));
|
|
};
|
|
|
|
watch(
|
|
() => examenStore.preguntas,
|
|
(nuevas) => {
|
|
if (!Array.isArray(nuevas) || nuevas.length === 0) return;
|
|
if (preguntasLocal.value.length === 0) {
|
|
preguntasLocal.value = transformarPreguntas(nuevas);
|
|
indiceActual.value = 0;
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
/* COMPUTEDS */
|
|
const totalPreguntas = computed(() => preguntasLocal.value.length);
|
|
const preguntaActual = computed(() => preguntasLocal.value[indiceActual.value] || null);
|
|
const esUltima = computed(() => indiceActual.value >= totalPreguntas.value - 1);
|
|
|
|
const preguntasRespondidas = computed(() => preguntasLocal.value.filter((p) => p.estado === "respondida").length);
|
|
|
|
const porcentajeCompletado = computed(() => {
|
|
if (!totalPreguntas.value) return 0;
|
|
return Math.round((preguntasRespondidas.value / totalPreguntas.value) * 100);
|
|
});
|
|
|
|
/* HELPERS */
|
|
const tieneOpciones = (p) => !!(p?.opciones && p.opciones.length);
|
|
|
|
const getLetraOpcion = (key) => {
|
|
const letras = ["A", "B", "C", "D", "E", "F", "G", "H"];
|
|
return letras[key] || `Opción ${key}`;
|
|
};
|
|
|
|
const tieneRespuestaLocal = (p) => {
|
|
if (!p) return false;
|
|
if (tieneOpciones(p)) return !!p.respuestaSeleccionada;
|
|
return !!(p.respuestaTexto && p.respuestaTexto.trim());
|
|
};
|
|
|
|
const estadoTexto = (p) => {
|
|
if (p.estado === "respondida") return "Guardada";
|
|
if (tieneRespuestaLocal(p)) return "Sin guardar";
|
|
return "Pendiente";
|
|
};
|
|
|
|
/* NAV */
|
|
const irAnterior = () => {
|
|
if (indiceActual.value > 0) indiceActual.value--;
|
|
};
|
|
|
|
const guardarRespuestaDePregunta = async (p) => {
|
|
if (!p) return { success: false, message: "No hay pregunta" };
|
|
|
|
if (p.estado === "respondida") return { success: true };
|
|
|
|
let respuesta = null;
|
|
|
|
if (tieneOpciones(p)) {
|
|
if (p.respuestaSeleccionada) {
|
|
const sel = p.respuestaSeleccionada.toString();
|
|
const op = p.opcionesOrdenadas.find((x) => x.key.toString() === sel);
|
|
respuesta = op ? op.texto : sel;
|
|
}
|
|
} else {
|
|
if (p.respuestaTexto && p.respuestaTexto.trim()) {
|
|
respuesta = p.respuestaTexto.trim();
|
|
}
|
|
}
|
|
|
|
return await examenStore.responderPregunta(p.id, respuesta);
|
|
};
|
|
|
|
/* IMÁGENES */
|
|
const normalizarImagenes = (imagenes) => {
|
|
if (!imagenes) return [];
|
|
if (Array.isArray(imagenes)) return imagenes.filter(Boolean).map(String);
|
|
|
|
if (typeof imagenes === "string") {
|
|
const s = imagenes.trim();
|
|
if (!s) return [];
|
|
|
|
if ((s.startsWith("[") && s.endsWith("]")) || (s.startsWith("{") && s.endsWith("}"))) {
|
|
try {
|
|
const parsed = JSON.parse(s);
|
|
if (Array.isArray(parsed)) return parsed.filter(Boolean).map(String);
|
|
if (parsed?.imagenes && Array.isArray(parsed.imagenes)) return parsed.imagenes.filter(Boolean).map(String);
|
|
} catch (_) {}
|
|
}
|
|
return [s];
|
|
}
|
|
return [];
|
|
};
|
|
|
|
const imagenesPregunta = computed(() => normalizarImagenes(preguntaActual.value?.imagenes));
|
|
|
|
/* ACCIÓN SIGUIENTE */
|
|
const siguienteAccion = async () => {
|
|
const p = preguntaActual.value;
|
|
if (!p) return;
|
|
|
|
try {
|
|
guardando.value = true;
|
|
|
|
const r = await guardarRespuestaDePregunta(p);
|
|
if (!r?.success) {
|
|
message.warning(r?.message || "Completa tu respuesta antes de continuar");
|
|
return;
|
|
}
|
|
|
|
p.estado = "respondida";
|
|
|
|
if (!esUltima.value) {
|
|
indiceActual.value++;
|
|
return;
|
|
}
|
|
|
|
await finalizarExamen();
|
|
} catch (e) {
|
|
console.error(e);
|
|
message.error("Error al guardar");
|
|
} finally {
|
|
guardando.value = false;
|
|
}
|
|
};
|
|
|
|
const finalizarExamen = async () => {
|
|
const examenId = Number(route.params.examenId);
|
|
|
|
const modal = Modal.info({
|
|
title: "Procesando calificación",
|
|
content: h("div", { style: "display:flex;align-items:center;gap:12px;" }, [
|
|
h(Spin, { size: "large" }),
|
|
h("div", [
|
|
h("div", { style: "font-weight:600;" }, "Finalizando evaluación"),
|
|
h("div", { style: "margin-top:4px;color:rgba(0,0,0,.65);" }, "Estamos calificando sus respuestas. Por favor, espere..."),
|
|
]),
|
|
]),
|
|
maskClosable: false,
|
|
closable: false,
|
|
okButtonProps: { style: { display: "none" } },
|
|
});
|
|
|
|
try {
|
|
finalizando.value = true;
|
|
|
|
const r = await examenStore.finalizarExamen(examenId);
|
|
|
|
const msg = (r?.message || "").toLowerCase();
|
|
if (!r?.success && (msg.includes("ya está finalizado") || msg.includes("ya esta finalizado"))) {
|
|
modal.update({
|
|
title: "Evaluación finalizada",
|
|
content: "El examen ya se encuentra finalizado. Redirigiendo a la página de resultados...",
|
|
okButtonProps: { style: { display: "none" } },
|
|
});
|
|
|
|
setTimeout(() => {
|
|
modal.destroy();
|
|
router.push({ name: "PanelResultados", params: { examenId } });
|
|
}, 1200);
|
|
|
|
return;
|
|
}
|
|
|
|
if (!r?.success) {
|
|
modal.update({
|
|
title: "No se pudo finalizar",
|
|
content: r?.message || "No fue posible finalizar el examen. Inténtelo nuevamente.",
|
|
okButtonProps: { style: { display: "none" } },
|
|
});
|
|
setTimeout(() => modal.destroy(), 1800);
|
|
return;
|
|
}
|
|
|
|
setTimeout(() => {
|
|
modal.update({
|
|
title: "Calificación completada",
|
|
content: "Redirigiendo a la página de resultados...",
|
|
okButtonProps: { style: { display: "none" } },
|
|
});
|
|
|
|
setTimeout(() => {
|
|
modal.destroy();
|
|
router.push({ name: "PanelResultados", params: { examenId } });
|
|
}, 900);
|
|
}, 5000);
|
|
} catch (e) {
|
|
modal.update({
|
|
title: "Error",
|
|
content: "Ocurrió un problema al finalizar el examen. Por favor, inténtelo nuevamente.",
|
|
okButtonProps: { style: { display: "none" } },
|
|
});
|
|
setTimeout(() => modal.destroy(), 2000);
|
|
} finally {
|
|
setTimeout(() => {
|
|
finalizando.value = false;
|
|
}, 5000);
|
|
}
|
|
};
|
|
|
|
const finalizarExamenAutomaticamente = () => {
|
|
message.error("Tiempo agotado. El examen se finalizará.");
|
|
finalizarExamen();
|
|
};
|
|
|
|
const calcularTiempoRestante = () => {
|
|
if (examenStore.examenActual?.hora_inicio && examenInfo.value.duracion) {
|
|
const inicio = new Date(examenStore.examenActual.hora_inicio);
|
|
const fin = inicio.getTime() + examenInfo.value.duracion * 60 * 1000;
|
|
timerValue.value = fin;
|
|
if (Date.now() > fin) finalizarExamenAutomaticamente();
|
|
} else {
|
|
timerValue.value = Date.now() + examenInfo.value.duracion * 60 * 1000;
|
|
}
|
|
};
|
|
|
|
const iniciarSesionExamen = async () => {
|
|
if (initOnce.value) return;
|
|
initOnce.value = true;
|
|
|
|
const examenId = route.params.examenId;
|
|
if (!examenId) {
|
|
message.error("No se encontró el examen");
|
|
router.push({ name: "DashboardPostulante" });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
cargandoInicio.value = true;
|
|
|
|
const r = await examenStore.iniciarExamen(examenId);
|
|
if (!r?.success) {
|
|
message.error(r?.message || "No se pudo iniciar el examen");
|
|
router.push({ name: "DashboardPostulante" });
|
|
return;
|
|
}
|
|
|
|
if (Array.isArray(examenStore.preguntas) && examenStore.preguntas.length) {
|
|
preguntasLocal.value = transformarPreguntas(examenStore.preguntas);
|
|
indiceActual.value = 0;
|
|
} else {
|
|
message.error("No hay preguntas");
|
|
router.push({ name: "DashboardPostulante" });
|
|
return;
|
|
}
|
|
|
|
calcularTiempoRestante();
|
|
if (timerIntervalId) clearInterval(timerIntervalId);
|
|
timerIntervalId = setInterval(calcularTiempoRestante, 30000);
|
|
} finally {
|
|
cargandoInicio.value = false;
|
|
}
|
|
};
|
|
|
|
onMounted(iniciarSesionExamen);
|
|
|
|
onBeforeUnmount(() => {
|
|
if (timerIntervalId) clearInterval(timerIntervalId);
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.exam-page {
|
|
max-width: 980px;
|
|
margin: 0 auto;
|
|
padding: 18px;
|
|
background: #f5f7fb;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
/* HEADER */
|
|
.top-card {
|
|
border-radius: 16px;
|
|
background: #ffffff;
|
|
border: 1px solid rgba(17, 24, 39, 0.08);
|
|
box-shadow: 0 10px 24px rgba(17, 24, 39, 0.06);
|
|
overflow: hidden;
|
|
}
|
|
.top-card::before {
|
|
content: "";
|
|
display: block;
|
|
height: 6px;
|
|
background: linear-gradient(90deg, #111827, #374151);
|
|
}
|
|
|
|
.topbar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 18px;
|
|
flex-wrap: wrap;
|
|
align-items: flex-start;
|
|
padding: 14px 16px 16px;
|
|
}
|
|
|
|
.top-left {
|
|
flex: 1;
|
|
min-width: 280px;
|
|
}
|
|
|
|
.sub {
|
|
margin-top: 6px;
|
|
color: #475569;
|
|
font-size: 12px;
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
}
|
|
|
|
.area {
|
|
font-weight: 800;
|
|
color: #0f172a;
|
|
}
|
|
|
|
.sep {
|
|
opacity: 0.55;
|
|
}
|
|
|
|
.meta {
|
|
margin-top: 10px;
|
|
font-size: 12px;
|
|
color: #64748b;
|
|
display: flex;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.progress {
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.progressText {
|
|
margin-top: 6px;
|
|
font-size: 12px;
|
|
color: #64748b;
|
|
}
|
|
|
|
/* top-right */
|
|
.top-right {
|
|
min-width: 220px;
|
|
text-align: right;
|
|
display: grid;
|
|
gap: 10px;
|
|
justify-items: end;
|
|
}
|
|
|
|
.statusPill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 10px;
|
|
border-radius: 999px;
|
|
border: 1px solid rgba(17, 24, 39, 0.10);
|
|
background: #f8fafc;
|
|
color: #334155;
|
|
font-size: 12px;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.statusPill .dot {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 99px;
|
|
background: #f59e0b;
|
|
}
|
|
.statusPill .dot.ok {
|
|
background: #22c55e;
|
|
}
|
|
|
|
.timerBox {
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
border-radius: 14px;
|
|
border: 1px solid rgba(17, 24, 39, 0.10);
|
|
background: #ffffff;
|
|
}
|
|
|
|
.timerLabel {
|
|
font-size: 12px;
|
|
color: #64748b;
|
|
font-weight: 800;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.timer :deep(.ant-statistic-content) {
|
|
font-size: 28px;
|
|
font-weight: 1000;
|
|
color: #0f172a;
|
|
letter-spacing: 0.6px;
|
|
}
|
|
|
|
/* CARDS */
|
|
.card {
|
|
margin-top: 14px;
|
|
border-radius: 18px;
|
|
background: #fff;
|
|
border: 1px solid rgba(17, 24, 39, 0.08);
|
|
box-shadow: 0 8px 20px rgba(17, 24, 39, 0.05);
|
|
}
|
|
|
|
.question-card {
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
/* PREGUNTA */
|
|
.q-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 14px 16px 6px;
|
|
}
|
|
|
|
.q-number {
|
|
font-weight: 1000;
|
|
color: #0f172a;
|
|
font-size: 15px;
|
|
}
|
|
|
|
.q-tags {
|
|
margin-top: 8px;
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
height: 26px;
|
|
padding: 0 10px;
|
|
border-radius: 999px;
|
|
background: #eef2ff;
|
|
color: #1f2937;
|
|
font-size: 12px;
|
|
font-weight: 900;
|
|
border: 1px solid rgba(17, 24, 39, 0.08);
|
|
}
|
|
|
|
.chip.muted {
|
|
background: #f1f5f9;
|
|
color: #334155;
|
|
}
|
|
|
|
/* divisor bajo el header de pregunta */
|
|
.custom-divider {
|
|
margin: 0 16px 8px;
|
|
height: 1px;
|
|
background: rgba(15, 23, 42, 0.10);
|
|
}
|
|
|
|
/* ===== ENUNCIADO SIN MARCO (solo texto + aire) ===== */
|
|
.qBody {
|
|
margin: 0 16px 10px;
|
|
}
|
|
|
|
.qStatement,
|
|
.qExtra {
|
|
padding: 8px 0;
|
|
line-height: 1.75;
|
|
color: #0f172a;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
/* Separador tipo examen (solo línea) */
|
|
.lineDivider {
|
|
margin: 12px 0 6px;
|
|
height: 1px;
|
|
background: rgba(15, 23, 42, 0.10);
|
|
}
|
|
|
|
/* KaTeX: aire arriba/abajo sin “flechas” */
|
|
.qBody :deep(.katex-display) {
|
|
margin: 14px 0;
|
|
padding: 8px 0;
|
|
overflow-x: auto;
|
|
overflow-y: hidden;
|
|
scrollbar-width: none;
|
|
}
|
|
.qBody :deep(.katex-display::-webkit-scrollbar) {
|
|
height: 0px;
|
|
}
|
|
|
|
/* IMÁGENES */
|
|
.imgWrap { margin-top: 10px; }
|
|
.imgGrid {
|
|
display: grid;
|
|
grid-template-columns: repeat(1, minmax(0, 520px));
|
|
gap: 10px;
|
|
justify-content: center;
|
|
}
|
|
.img :deep(img) {
|
|
width: 100%;
|
|
height: 260px;
|
|
object-fit: contain;
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(17, 24, 39, 0.10);
|
|
background: #fff;
|
|
}
|
|
|
|
/* ===== OPCIONES SIN MARCOS: solo líneas ===== */
|
|
.answer {
|
|
margin: 0 16px;
|
|
padding-bottom: 6px;
|
|
}
|
|
|
|
.radio-group { width: 100%; }
|
|
|
|
.optList {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* cada opción = una fila con solo línea abajo */
|
|
.optRow {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 10px;
|
|
padding: 12px 2px;
|
|
margin: 0;
|
|
border-bottom: 1px solid rgba(15, 23, 42, 0.10);
|
|
}
|
|
|
|
/* radio alineado */
|
|
.optRow :deep(.ant-radio) {
|
|
align-self: flex-start;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
/* A) y texto en una sola fila */
|
|
.optKey {
|
|
flex: 0 0 auto;
|
|
min-width: 28px;
|
|
font-weight: 1000;
|
|
color: #0f172a;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.optTextInline {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
color: #0f172a;
|
|
line-height: 1.6;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
/* seleccionado (sin caja) */
|
|
.optRow:has(:deep(.ant-radio-checked)) {
|
|
background: rgba(37, 99, 235, 0.06);
|
|
}
|
|
|
|
/* hover suave */
|
|
.optRow:hover {
|
|
background: rgba(15, 23, 42, 0.03);
|
|
}
|
|
|
|
/* Textarea */
|
|
:deep(.ant-input) {
|
|
border-radius: 12px;
|
|
}
|
|
:deep(.ant-input:focus),
|
|
:deep(.ant-input-focused) {
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
|
|
}
|
|
|
|
/* NAV */
|
|
.nav {
|
|
margin: 16px 16px 0;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
padding-bottom: 6px;
|
|
}
|
|
|
|
.btn {
|
|
height: 42px;
|
|
border-radius: 12px;
|
|
font-weight: 900;
|
|
min-width: 150px;
|
|
}
|
|
|
|
.primary {
|
|
min-width: 230px;
|
|
}
|
|
|
|
/* Nota */
|
|
.note {
|
|
margin: 12px 16px 16px;
|
|
font-size: 12px;
|
|
color: #64748b;
|
|
line-height: 1.6;
|
|
padding: 10px 12px;
|
|
border-radius: 12px;
|
|
background: #f8fafc;
|
|
border: 1px dashed rgba(17, 24, 39, 0.14);
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 640px) {
|
|
.exam-page {
|
|
padding: 10px;
|
|
}
|
|
|
|
.top-right {
|
|
text-align: left;
|
|
justify-items: start;
|
|
min-width: 100%;
|
|
}
|
|
|
|
.qBody,
|
|
.answer {
|
|
margin: 0 12px;
|
|
}
|
|
|
|
.nav {
|
|
flex-direction: column;
|
|
margin: 14px 12px 0;
|
|
}
|
|
|
|
.btn,
|
|
.primary {
|
|
width: 100%;
|
|
min-width: 0;
|
|
}
|
|
|
|
.img :deep(img) {
|
|
height: 220px;
|
|
}
|
|
|
|
.optRow {
|
|
padding: 12px 0;
|
|
}
|
|
}
|
|
</style> |