feature: initial simulator
This commit is contained in:
@@ -0,0 +1,489 @@
|
|||||||
|
[v-cloak] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-simulator {
|
||||||
|
--exam-blue: #47abe8;
|
||||||
|
--exam-coral: #fe756c;
|
||||||
|
--exam-green: #3bb583;
|
||||||
|
--exam-ink: #20252a;
|
||||||
|
color: var(--exam-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-loading {
|
||||||
|
min-height: 45vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-kicker {
|
||||||
|
margin: 0 0 0.25rem;
|
||||||
|
color: #0068a5;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-start {
|
||||||
|
max-width: 1050px;
|
||||||
|
margin: 1.5rem auto 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-choice-groups {
|
||||||
|
display: grid;
|
||||||
|
gap: 2.5rem;
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-choice-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-choice {
|
||||||
|
min-height: 155px;
|
||||||
|
padding: 1.4rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
text-align: left;
|
||||||
|
background: var(--exam-choice-color);
|
||||||
|
border: 3px solid var(--exam-choice-color);
|
||||||
|
border-radius: 0.65rem;
|
||||||
|
box-shadow: 0 0.35rem 1rem rgb(0 0 0 / 10%);
|
||||||
|
transition: transform 120ms ease, box-shadow 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-choice:hover,
|
||||||
|
.exam-choice:focus-visible {
|
||||||
|
color: #fff;
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 0.55rem 1.4rem rgb(0 0 0 / 18%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-choice:focus-visible {
|
||||||
|
outline: 3px solid #20252a;
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-choice strong {
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-choice span {
|
||||||
|
margin-top: 0.55rem;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-choice small {
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-start-note {
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
padding: 1.2rem 1.4rem;
|
||||||
|
background: #eef8fd;
|
||||||
|
border-left: 5px solid #00adef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-session {
|
||||||
|
width: min(1560px, calc(100vw - 2rem));
|
||||||
|
position: relative;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-toolbar {
|
||||||
|
margin: 1rem 0 1.25rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1.5rem;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c9d1d7;
|
||||||
|
border-top: 6px solid #00adef;
|
||||||
|
box-shadow: 0 0.25rem 0.8rem rgb(0 0 0 / 7%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-toolbar h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(1.35rem, 2vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-progress-label {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
color: #606a73;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-toolbar-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-timer {
|
||||||
|
min-width: 135px;
|
||||||
|
padding: 0.55rem 0.8rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
color: #fff;
|
||||||
|
background: #20252a;
|
||||||
|
border-radius: 0.35rem;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-timer-warning {
|
||||||
|
background: #b42d25;
|
||||||
|
animation: exam-timer-pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-timer-finished {
|
||||||
|
background: #5c636a;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes exam-timer-pulse {
|
||||||
|
50% { box-shadow: 0 0 0 5px rgb(254 117 108 / 30%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-part-result,
|
||||||
|
.exam-overall {
|
||||||
|
margin: 0 0 1.25rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
border: 2px solid;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-part-result > .material-icons,
|
||||||
|
.exam-overall > .material-icons {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-passed {
|
||||||
|
background: #e2f5ed;
|
||||||
|
border-color: #3bb583;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-oral {
|
||||||
|
background: #fff4d5;
|
||||||
|
border-color: #e6a700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-failed,
|
||||||
|
.result-incomplete {
|
||||||
|
background: #ffe5e3;
|
||||||
|
border-color: #fe756c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-paper-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 360px minmax(0, 1fr);
|
||||||
|
align-items: start;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-answer-sheet {
|
||||||
|
position: sticky;
|
||||||
|
top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-sheet-paper {
|
||||||
|
padding: 1rem;
|
||||||
|
background: #fff;
|
||||||
|
border: 2px solid #20252a;
|
||||||
|
box-shadow: 0 0.35rem 1rem rgb(0 0 0 / 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-sheet-paper h2 {
|
||||||
|
margin: 0 0 0.8rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-sheet-part {
|
||||||
|
padding: 0.6rem 0;
|
||||||
|
display: grid;
|
||||||
|
border-top: 2px solid #20252a;
|
||||||
|
border-bottom: 2px solid #20252a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-sheet-part span,
|
||||||
|
.answer-sheet-part small {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-sheet-part strong {
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-sheet-hint {
|
||||||
|
margin: 0.6rem 0;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-table th,
|
||||||
|
.answer-table td {
|
||||||
|
height: 27px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid #20252a;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-table thead th,
|
||||||
|
.answer-table tbody th {
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-table tr > :first-child {
|
||||||
|
width: 18%;
|
||||||
|
border-right-width: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-number,
|
||||||
|
.answer-mark {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
color: #20252a;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-number:hover,
|
||||||
|
.answer-number:focus-visible,
|
||||||
|
.answer-mark:not(:disabled):hover,
|
||||||
|
.answer-mark:not(:disabled):focus-visible {
|
||||||
|
background: #d2f3ff;
|
||||||
|
outline: 2px solid #0068a5;
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-mark:disabled {
|
||||||
|
color: #20252a;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-mark-symbol {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 1rem;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-mark-restored {
|
||||||
|
padding-bottom: 1px;
|
||||||
|
border-bottom: 3px double currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-cell-correct {
|
||||||
|
background: #bfe8d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-cell-wrong {
|
||||||
|
background: #ffc9c5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-cell-solution {
|
||||||
|
box-shadow: inset 0 0 0 2px #168a5d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-sheet-count {
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-questions {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-question.question {
|
||||||
|
scroll-margin-top: 1rem;
|
||||||
|
margin: 0 0 1.25rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-left: 5px solid #7c8790;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-question .answers button {
|
||||||
|
display: block;
|
||||||
|
min-height: 48px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-question .answers button::before {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-question .answers button:not(:disabled):hover,
|
||||||
|
.exam-question .answers button:not(:disabled):focus-visible,
|
||||||
|
.exam-question .answers button.exam-answer-selected {
|
||||||
|
border-color: #0068a5;
|
||||||
|
background: #d2f3ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-question .answers button:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 1;
|
||||||
|
color: #20252a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-question .answers button.correct {
|
||||||
|
background: #3bb583;
|
||||||
|
border-color: #168a5d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-question .answers button.wrong {
|
||||||
|
background: #fe756c;
|
||||||
|
border-color: #c5423a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-answer-label {
|
||||||
|
margin-right: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-solution-icon {
|
||||||
|
margin-right: 0.3rem;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-submit-panel {
|
||||||
|
margin: 2rem 0 4rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
background: #eef2f5;
|
||||||
|
border: 1px solid #c9d1d7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-submit-panel p {
|
||||||
|
flex-basis: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-summary {
|
||||||
|
max-width: 920px;
|
||||||
|
margin: 2rem auto 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-result-list {
|
||||||
|
margin: 2rem 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-result-card {
|
||||||
|
padding: 0.9rem 1rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(220px, 1fr) auto minmax(150px, auto) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
border: 1px solid #c9d1d7;
|
||||||
|
border-left: 5px solid #00adef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-result-card > div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-result-code {
|
||||||
|
width: 2.2rem;
|
||||||
|
height: 2.2rem;
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
color: #fff;
|
||||||
|
background: #20252a;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-score {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-status {
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1050px) {
|
||||||
|
.exam-paper-layout {
|
||||||
|
grid-template-columns: 310px minmax(0, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.exam-choice-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-choice {
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-toolbar,
|
||||||
|
.exam-toolbar-actions {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-timer {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-paper-layout {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-answer-sheet {
|
||||||
|
width: 100%;
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-sheet-paper {
|
||||||
|
max-width: 430px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-result-card {
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exam-result-card .exam-status {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,501 @@
|
|||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var STORAGE_KEY = "50ohm-exam-simulator-v1";
|
||||||
|
var STORAGE_VERSION = 1;
|
||||||
|
var QUESTION_COUNT = 25;
|
||||||
|
var ANSWER_LABELS = ["A", "B", "C", "D"];
|
||||||
|
|
||||||
|
var PART_DEFINITIONS = {
|
||||||
|
B: { label: "Betriebliche Kenntnisse", formLabel: "Betriebliche Kenntnisse", minutes: 45 },
|
||||||
|
V: { label: "Kenntnisse von Vorschriften", formLabel: "Kenntnisse von Vorschriften", minutes: 45 },
|
||||||
|
N: { label: "Technische Kenntnisse Klasse N", formLabel: "Technische Kenntnisse Klasse N", minutes: 45 },
|
||||||
|
E: { label: "Technische Kenntnisse Klasse E", formLabel: "Technische Kenntnisse Klasse E", minutes: 45 },
|
||||||
|
A: { label: "Technische Kenntnisse Klasse A", formLabel: "Technische Kenntnisse Klasse A", minutes: 60 }
|
||||||
|
};
|
||||||
|
|
||||||
|
var EXAM_CHOICES = [
|
||||||
|
{ id: "N", group: "primary", shortLabel: "N", label: "Prüfung Klasse N", parts: ["B", "V", "N"], color: "#47ABE8" },
|
||||||
|
{ id: "E", group: "primary", shortLabel: "N + E", label: "Prüfung Klasse E", parts: ["B", "V", "N", "E"], color: "#FE756C" },
|
||||||
|
{ id: "A", group: "primary", shortLabel: "N + E + A", label: "Prüfung Klasse A", parts: ["B", "V", "N", "E", "A"], color: "#3BB583" },
|
||||||
|
{ id: "N-E", group: "upgrade", shortLabel: "N → E", label: "Aufstockung von N auf E", parts: ["E"], color: "#FE756C" },
|
||||||
|
{ id: "E-A", group: "upgrade", shortLabel: "E → A", label: "Aufstockung von E auf A", parts: ["A"], color: "#3BB583" },
|
||||||
|
{ id: "N-A", group: "upgrade", shortLabel: "N → A", label: "Aufstockung von N auf A", parts: ["E", "A"], color: "#3BB583" }
|
||||||
|
];
|
||||||
|
|
||||||
|
function shuffled(values) {
|
||||||
|
var result = values.slice();
|
||||||
|
for (var i = result.length - 1; i > 0; i -= 1) {
|
||||||
|
var j = Math.floor(Math.random() * (i + 1));
|
||||||
|
var tmp = result[i];
|
||||||
|
result[i] = result[j];
|
||||||
|
result[j] = tmp;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCategoryTree(pool) {
|
||||||
|
var root = { children: new Map(), items: [], size: 0 };
|
||||||
|
pool.forEach(function (item) {
|
||||||
|
var node = root;
|
||||||
|
node.size += 1;
|
||||||
|
(item.category || []).forEach(function (categoryPart) {
|
||||||
|
if (!node.children.has(categoryPart)) {
|
||||||
|
node.children.set(categoryPart, { children: new Map(), items: [], size: 0 });
|
||||||
|
}
|
||||||
|
node = node.children.get(categoryPart);
|
||||||
|
node.size += 1;
|
||||||
|
});
|
||||||
|
node.items.push(item);
|
||||||
|
});
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
function allocateQuotas(children, quota, totalSize) {
|
||||||
|
var allocations = children.map(function (child) {
|
||||||
|
var target = quota * child.size / totalSize;
|
||||||
|
return { child: child, quota: Math.floor(target), fraction: target - Math.floor(target) };
|
||||||
|
});
|
||||||
|
var assigned = allocations.reduce(function (sum, allocation) { return sum + allocation.quota; }, 0);
|
||||||
|
|
||||||
|
while (assigned < quota) {
|
||||||
|
var candidates = allocations.filter(function (allocation) {
|
||||||
|
return allocation.quota < allocation.child.size;
|
||||||
|
});
|
||||||
|
var weight = candidates.reduce(function (sum, allocation) { return sum + allocation.fraction; }, 0);
|
||||||
|
var selected;
|
||||||
|
if (weight <= 0) {
|
||||||
|
selected = candidates[Math.floor(Math.random() * candidates.length)];
|
||||||
|
} else {
|
||||||
|
var draw = Math.random() * weight;
|
||||||
|
selected = candidates[candidates.length - 1];
|
||||||
|
for (var i = 0; i < candidates.length; i += 1) {
|
||||||
|
draw -= candidates[i].fraction;
|
||||||
|
if (draw <= 0) {
|
||||||
|
selected = candidates[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selected.quota += 1;
|
||||||
|
selected.fraction = 0;
|
||||||
|
assigned += 1;
|
||||||
|
}
|
||||||
|
return allocations;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickFromTree(node, quota) {
|
||||||
|
if (quota <= 0) return [];
|
||||||
|
if (node.children.size === 0) return shuffled(node.items).slice(0, quota);
|
||||||
|
var allocations = allocateQuotas(Array.from(node.children.values()), quota, node.size);
|
||||||
|
var result = [];
|
||||||
|
allocations.forEach(function (allocation) {
|
||||||
|
result = result.concat(pickFromTree(allocation.child, allocation.quota));
|
||||||
|
});
|
||||||
|
return shuffled(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function balancedSample(pool, count) {
|
||||||
|
if (!Array.isArray(pool) || pool.length < count) {
|
||||||
|
throw new Error("Für diesen Prüfungsteil stehen nicht genügend Fragen zur Verfügung.");
|
||||||
|
}
|
||||||
|
return pickFromTree(buildCategoryTree(pool), count);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMathSoon(VueApi) {
|
||||||
|
VueApi.nextTick(function () {
|
||||||
|
if (typeof window.renderFiftyOhmMath === "function") {
|
||||||
|
window.renderFiftyOhmMath(document.getElementById("exam-simulator"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var app = Vue.createApp({
|
||||||
|
setup: function () {
|
||||||
|
var ref = Vue.ref;
|
||||||
|
var computed = Vue.computed;
|
||||||
|
var watch = Vue.watch;
|
||||||
|
var onMounted = Vue.onMounted;
|
||||||
|
var onBeforeUnmount = Vue.onBeforeUnmount;
|
||||||
|
|
||||||
|
var loading = ref(true);
|
||||||
|
var loadError = ref("");
|
||||||
|
var catalog = ref({});
|
||||||
|
var session = ref(null);
|
||||||
|
var view = ref("exam");
|
||||||
|
var reviewPartIndex = ref(null);
|
||||||
|
var now = ref(Date.now());
|
||||||
|
var timerId = null;
|
||||||
|
|
||||||
|
var primaryChoices = computed(function () {
|
||||||
|
return EXAM_CHOICES.filter(function (choice) { return choice.group === "primary"; });
|
||||||
|
});
|
||||||
|
var upgradeChoices = computed(function () {
|
||||||
|
return EXAM_CHOICES.filter(function (choice) { return choice.group === "upgrade"; });
|
||||||
|
});
|
||||||
|
var currentChoice = computed(function () {
|
||||||
|
if (!session.value) return null;
|
||||||
|
return EXAM_CHOICES.find(function (choice) { return choice.id === session.value.choiceId; });
|
||||||
|
});
|
||||||
|
var displayedPartIndex = computed(function () {
|
||||||
|
return reviewPartIndex.value === null ? session.value.currentPart : reviewPartIndex.value;
|
||||||
|
});
|
||||||
|
var currentPart = computed(function () {
|
||||||
|
if (!session.value) return null;
|
||||||
|
return session.value.parts[displayedPartIndex.value];
|
||||||
|
});
|
||||||
|
var currentPartDefinition = computed(function () {
|
||||||
|
if (!currentPart.value) return null;
|
||||||
|
return PART_DEFINITIONS[currentPart.value.code];
|
||||||
|
});
|
||||||
|
var remainingSeconds = computed(function () {
|
||||||
|
if (!currentPart.value || currentPart.value.status !== "running") return 0;
|
||||||
|
return Math.max(0, Math.ceil((currentPart.value.deadline - now.value) / 1000));
|
||||||
|
});
|
||||||
|
var formattedTime = computed(function () {
|
||||||
|
var minutes = Math.floor(remainingSeconds.value / 60);
|
||||||
|
var seconds = remainingSeconds.value % 60;
|
||||||
|
return String(minutes).padStart(2, "0") + ":" + String(seconds).padStart(2, "0");
|
||||||
|
});
|
||||||
|
var answeredCount = computed(function () {
|
||||||
|
if (!currentPart.value) return 0;
|
||||||
|
return currentPart.value.questions.filter(function (question) { return question.selected !== null; }).length;
|
||||||
|
});
|
||||||
|
var hasNextPart = computed(function () {
|
||||||
|
if (!session.value) return false;
|
||||||
|
return session.value.currentPart < session.value.parts.length - 1;
|
||||||
|
});
|
||||||
|
var isReviewing = computed(function () { return reviewPartIndex.value !== null; });
|
||||||
|
|
||||||
|
function evaluatedParts() {
|
||||||
|
return session.value.parts.filter(function (part) { return part.status === "evaluated"; });
|
||||||
|
}
|
||||||
|
|
||||||
|
var overallState = computed(function () {
|
||||||
|
if (session.value.parts.some(function (part) { return part.status !== "evaluated"; })) return "incomplete";
|
||||||
|
var failed = session.value.parts.filter(function (part) { return part.score < 19; });
|
||||||
|
if (failed.length === 0) return "passed";
|
||||||
|
if (failed.length === 1 && failed[0].score >= 17) return "oral";
|
||||||
|
return "failed";
|
||||||
|
});
|
||||||
|
var overallClass = computed(function () { return "result-" + overallState.value; });
|
||||||
|
var overallIcon = computed(function () {
|
||||||
|
return { passed: "verified", oral: "record_voice_over", failed: "cancel", incomplete: "stop_circle" }[overallState.value];
|
||||||
|
});
|
||||||
|
var overallTitle = computed(function () {
|
||||||
|
return { passed: "Prüfung bestanden", oral: "Mündliche Nachprüfung möglich", failed: "Prüfung nicht bestanden", incomplete: "Prüfung vorzeitig beendet" }[overallState.value];
|
||||||
|
});
|
||||||
|
var overallText = computed(function () {
|
||||||
|
if (overallState.value === "passed") return "Du hast jeden Prüfungsteil mit mindestens 19 Punkten bestanden.";
|
||||||
|
if (overallState.value === "oral") return "In genau einem Prüfungsteil wurden 17 oder 18 Punkte erreicht; nach den Prüfungsregeln kann eine mündliche Nachprüfung möglich sein.";
|
||||||
|
if (overallState.value === "failed") return "Mindestens ein Prüfungsteil wurde nicht bestanden.";
|
||||||
|
return evaluatedParts().length + " von " + session.value.parts.length + " Prüfungsteilen wurden abgeschlossen.";
|
||||||
|
});
|
||||||
|
|
||||||
|
function questionData(questionState) {
|
||||||
|
return catalog.value[questionState.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
function partSummary(parts) {
|
||||||
|
return parts.join(" + ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function padNumber(number) {
|
||||||
|
return String(number).padStart(2, "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPart(code) {
|
||||||
|
var picked = balancedSample(Object.values(catalog.value).filter(function (question) {
|
||||||
|
return question.part === code;
|
||||||
|
}), QUESTION_COUNT);
|
||||||
|
return {
|
||||||
|
code: code,
|
||||||
|
status: "pending",
|
||||||
|
deadline: null,
|
||||||
|
score: null,
|
||||||
|
autoSubmitted: false,
|
||||||
|
questions: picked.map(function (question) {
|
||||||
|
return { id: question.id, order: shuffled([0, 1, 2, 3]), selected: null, history: [] };
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function activatePart(part) {
|
||||||
|
part.status = "running";
|
||||||
|
part.deadline = Date.now() + PART_DEFINITIONS[part.code].minutes * 60 * 1000;
|
||||||
|
now.value = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startExam(choice) {
|
||||||
|
try {
|
||||||
|
var parts = choice.parts.map(createPart);
|
||||||
|
activatePart(parts[0]);
|
||||||
|
session.value = {
|
||||||
|
version: STORAGE_VERSION,
|
||||||
|
choiceId: choice.id,
|
||||||
|
currentPart: 0,
|
||||||
|
startedAt: Date.now(),
|
||||||
|
parts: parts
|
||||||
|
};
|
||||||
|
view.value = "exam";
|
||||||
|
reviewPartIndex.value = null;
|
||||||
|
renderMathSoon(Vue);
|
||||||
|
} catch (error) {
|
||||||
|
loadError.value = error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAnswer(questionIndex, answerIndex) {
|
||||||
|
if (currentPart.value.status !== "running") return;
|
||||||
|
var question = currentPart.value.questions[questionIndex];
|
||||||
|
if (question.selected === answerIndex) return;
|
||||||
|
question.selected = answerIndex;
|
||||||
|
question.history.push(answerIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function answerMark(questionIndex, answerIndex) {
|
||||||
|
var question = currentPart.value.questions[questionIndex];
|
||||||
|
if (question.selected === answerIndex) {
|
||||||
|
var restored = question.history.slice(0, -1).indexOf(answerIndex) !== -1;
|
||||||
|
return '<span class="answer-mark-symbol' + (restored ? " answer-mark-restored" : "") + '">×</span>';
|
||||||
|
}
|
||||||
|
if (question.history.indexOf(answerIndex) !== -1) {
|
||||||
|
return '<span class="answer-mark-symbol">=</span>';
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function answerCellClass(questionIndex, answerIndex) {
|
||||||
|
if (currentPart.value.status !== "evaluated") return "";
|
||||||
|
var question = currentPart.value.questions[questionIndex];
|
||||||
|
var sourceIndex = question.order[answerIndex];
|
||||||
|
if (question.selected === answerIndex && sourceIndex === 0) return "answer-cell-correct";
|
||||||
|
if (question.selected === answerIndex) return "answer-cell-wrong";
|
||||||
|
if (sourceIndex === 0) return "answer-cell-solution";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function questionAnswerClass(questionIndex, answerIndex) {
|
||||||
|
var question = currentPart.value.questions[questionIndex];
|
||||||
|
if (currentPart.value.status === "running") {
|
||||||
|
return question.selected === answerIndex ? "exam-answer-selected" : "";
|
||||||
|
}
|
||||||
|
var sourceIndex = question.order[answerIndex];
|
||||||
|
if (sourceIndex === 0) return "correct";
|
||||||
|
if (question.selected === answerIndex) return "wrong";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitPart(autoSubmitted) {
|
||||||
|
if (!session.value || currentPart.value.status !== "running") return;
|
||||||
|
var score = currentPart.value.questions.reduce(function (sum, question) {
|
||||||
|
return sum + (question.selected !== null && question.order[question.selected] === 0 ? 1 : 0);
|
||||||
|
}, 0);
|
||||||
|
currentPart.value.score = score;
|
||||||
|
currentPart.value.status = "evaluated";
|
||||||
|
currentPart.value.deadline = null;
|
||||||
|
currentPart.value.autoSubmitted = Boolean(autoSubmitted);
|
||||||
|
renderMathSoon(Vue);
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestSubmit() {
|
||||||
|
var unanswered = QUESTION_COUNT - answeredCount.value;
|
||||||
|
var text = unanswered > 0
|
||||||
|
? "Du hast " + unanswered + " Fragen nicht beantwortet. Möchtest du den Prüfungsteil trotzdem abgeben?"
|
||||||
|
: "Möchtest du diesen Prüfungsteil jetzt verbindlich abgeben?";
|
||||||
|
if (window.confirm(text)) submitPart(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startNextPart() {
|
||||||
|
if (!hasNextPart.value) return;
|
||||||
|
session.value.currentPart += 1;
|
||||||
|
activatePart(session.value.parts[session.value.currentPart]);
|
||||||
|
reviewPartIndex.value = null;
|
||||||
|
renderMathSoon(Vue);
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishExamEarly() {
|
||||||
|
if (!window.confirm("Möchtest du die Prüfung wirklich hier beenden? Nicht bearbeitete Teile gelten dann als nicht abgeschlossen.")) return;
|
||||||
|
showSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSummary() {
|
||||||
|
reviewPartIndex.value = null;
|
||||||
|
view.value = "summary";
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function reviewPart(index) {
|
||||||
|
reviewPartIndex.value = index;
|
||||||
|
view.value = "exam";
|
||||||
|
renderMathSoon(Vue);
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearStoredSession() {
|
||||||
|
try { window.localStorage.removeItem(STORAGE_KEY); } catch (error) { /* storage can be unavailable */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function abortExam() {
|
||||||
|
if (!window.confirm("Möchtest du diese Probeprüfung abbrechen? Der gespeicherte Prüfungsstand wird gelöscht.")) return;
|
||||||
|
clearStoredSession();
|
||||||
|
session.value = null;
|
||||||
|
view.value = "exam";
|
||||||
|
reviewPartIndex.value = null;
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function newExam() {
|
||||||
|
clearStoredSession();
|
||||||
|
session.value = null;
|
||||||
|
view.value = "exam";
|
||||||
|
reviewPartIndex.value = null;
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToQuestion(index) {
|
||||||
|
var element = document.getElementById("exam-question-" + index);
|
||||||
|
if (element) element.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function resultLabel(part) {
|
||||||
|
if (part.score >= 19) return "Bestanden";
|
||||||
|
if (part.score >= 17) return "Nachprüfung möglich";
|
||||||
|
return "Nicht bestanden";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resultClass(part) {
|
||||||
|
if (part.score >= 19) return "result-passed";
|
||||||
|
if (part.score >= 17) return "result-oral";
|
||||||
|
return "result-failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resultIcon(part) {
|
||||||
|
if (part.score >= 19) return "check_circle";
|
||||||
|
if (part.score >= 17) return "record_voice_over";
|
||||||
|
return "cancel";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resultDescription(part) {
|
||||||
|
var prefix = part.autoSubmitted ? "Die Zeit ist abgelaufen und der Prüfungsteil wurde automatisch abgegeben. " : "";
|
||||||
|
if (part.score >= 19) return prefix + "Die erforderlichen 19 Punkte wurden erreicht.";
|
||||||
|
if (part.score >= 17) return prefix + "Mit 17 oder 18 Punkten kann nach den Prüfungsregeln eine mündliche Nachprüfung möglich sein.";
|
||||||
|
return prefix + "Die erforderlichen 19 Punkte wurden nicht erreicht.";
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreSession() {
|
||||||
|
try {
|
||||||
|
var raw = window.localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!raw) return;
|
||||||
|
var stored = JSON.parse(raw);
|
||||||
|
if (!stored || stored.version !== STORAGE_VERSION || !stored.session) return;
|
||||||
|
var choice = EXAM_CHOICES.find(function (item) { return item.id === stored.session.choiceId; });
|
||||||
|
var valid = choice && stored.session.parts.every(function (part) {
|
||||||
|
return Array.isArray(part.questions) && part.questions.length === QUESTION_COUNT && part.questions.every(function (question) {
|
||||||
|
return Boolean(catalog.value[question.id]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (!valid) {
|
||||||
|
clearStoredSession();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
session.value = stored.session;
|
||||||
|
view.value = stored.view === "summary" ? "summary" : "exam";
|
||||||
|
if (session.value.parts[session.value.currentPart].status === "running" &&
|
||||||
|
session.value.parts[session.value.currentPart].deadline <= Date.now()) {
|
||||||
|
submitPart(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
clearStoredSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([session, view], function () {
|
||||||
|
if (!session.value) return;
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||||||
|
version: STORAGE_VERSION,
|
||||||
|
session: session.value,
|
||||||
|
view: view.value
|
||||||
|
}));
|
||||||
|
} catch (error) { /* simulator remains usable without persistence */ }
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
watch(remainingSeconds, function (seconds) {
|
||||||
|
if (session.value && currentPart.value && currentPart.value.status === "running" && seconds <= 0) submitPart(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(function () {
|
||||||
|
fetch("assets/exam-questions.json")
|
||||||
|
.then(function (response) {
|
||||||
|
if (!response.ok) throw new Error("Fragenkatalog nicht gefunden (HTTP " + response.status + ").");
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function (data) {
|
||||||
|
catalog.value = data.questions || {};
|
||||||
|
if (Object.keys(catalog.value).length === 0) throw new Error("Der Fragenkatalog ist leer.");
|
||||||
|
restoreSession();
|
||||||
|
loading.value = false;
|
||||||
|
renderMathSoon(Vue);
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
loadError.value = error.message || "Unbekannter Ladefehler";
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
timerId = window.setInterval(function () { now.value = Date.now(); }, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(function () {
|
||||||
|
if (timerId !== null) window.clearInterval(timerId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading: loading,
|
||||||
|
loadError: loadError,
|
||||||
|
session: session,
|
||||||
|
view: view,
|
||||||
|
primaryChoices: primaryChoices,
|
||||||
|
upgradeChoices: upgradeChoices,
|
||||||
|
currentChoice: currentChoice,
|
||||||
|
currentPart: currentPart,
|
||||||
|
currentPartDefinition: currentPartDefinition,
|
||||||
|
displayedPartIndex: displayedPartIndex,
|
||||||
|
partDefinitions: PART_DEFINITIONS,
|
||||||
|
answerLabels: ANSWER_LABELS,
|
||||||
|
remainingSeconds: remainingSeconds,
|
||||||
|
formattedTime: formattedTime,
|
||||||
|
answeredCount: answeredCount,
|
||||||
|
hasNextPart: hasNextPart,
|
||||||
|
isReviewing: isReviewing,
|
||||||
|
overallClass: overallClass,
|
||||||
|
overallIcon: overallIcon,
|
||||||
|
overallTitle: overallTitle,
|
||||||
|
overallText: overallText,
|
||||||
|
partSummary: partSummary,
|
||||||
|
padNumber: padNumber,
|
||||||
|
questionData: questionData,
|
||||||
|
startExam: startExam,
|
||||||
|
selectAnswer: selectAnswer,
|
||||||
|
answerMark: answerMark,
|
||||||
|
answerCellClass: answerCellClass,
|
||||||
|
questionAnswerClass: questionAnswerClass,
|
||||||
|
requestSubmit: requestSubmit,
|
||||||
|
startNextPart: startNextPart,
|
||||||
|
finishExamEarly: finishExamEarly,
|
||||||
|
showSummary: showSummary,
|
||||||
|
reviewPart: reviewPart,
|
||||||
|
abortExam: abortExam,
|
||||||
|
newExam: newExam,
|
||||||
|
scrollToQuestion: scrollToQuestion,
|
||||||
|
resultLabel: resultLabel,
|
||||||
|
resultClass: resultClass,
|
||||||
|
resultIcon: resultIcon,
|
||||||
|
resultDescription: resultDescription
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.mount("#exam-simulator");
|
||||||
|
})();
|
||||||
+14
-10
@@ -150,15 +150,19 @@ document.addEventListener("DOMContentLoaded", function() {
|
|||||||
"\\sps": "\\text{sps}",
|
"\\sps": "\\text{sps}",
|
||||||
};
|
};
|
||||||
|
|
||||||
renderMathInElement(document.body, {
|
window.renderFiftyOhmMath = function(root) {
|
||||||
delimiters: [
|
renderMathInElement(root || document.body, {
|
||||||
{ left: '$$', right: '$$', display: true }, // Note: $$ has to come before $
|
delimiters: [
|
||||||
{ left: '$', right: '$', display: false },
|
{ left: '$$', right: '$$', display: true }, // Note: $$ has to come before $
|
||||||
{ left: '\\[', right: '\\]', display: true },
|
{ left: '$', right: '$', display: false },
|
||||||
{ left: "\\(", right: "\\)", display: false },
|
{ left: '\\[', right: '\\]', display: true },
|
||||||
],
|
{ left: "\\(", right: "\\)", display: false },
|
||||||
throwOnError: false,
|
],
|
||||||
macros: macros
|
throwOnError: false,
|
||||||
});
|
macros: macros
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.renderFiftyOhmMath(document.body);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ function shuffleChildren(el) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
var els = document.querySelectorAll(".answers");
|
var els = document.querySelectorAll(".answers:not([data-no-shuffle])");
|
||||||
for (var i = 0; i < els.length; i++) {
|
for (var i = 0; i < els.length; i++) {
|
||||||
shuffleChildren(els[i]);
|
shuffleChildren(els[i]);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../50ohm-contents-dl"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
<div id="exam-simulator" class="exam-simulator" v-cloak>
|
||||||
|
<div v-if="loading" class="exam-loading" role="status">
|
||||||
|
<div class="spinner-border text-primary" aria-hidden="true"></div>
|
||||||
|
<span>Prüfungsfragen werden geladen …</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="loadError" class="alert alert-danger" role="alert">
|
||||||
|
<h1 class="h4">Der Prüfungssimulator konnte nicht geladen werden</h1>
|
||||||
|
<p class="mb-0" v-text="loadError"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section v-else-if="!session" class="exam-start" aria-labelledby="exam-heading">
|
||||||
|
<p class="exam-kicker">Probeprüfung</p>
|
||||||
|
<h1 id="exam-heading">Prüfungssimulator für Funkamateure</h1>
|
||||||
|
<p class="lead">Wähle die Prüfung, die du unter möglichst realistischen Bedingungen üben möchtest.</p>
|
||||||
|
|
||||||
|
<div class="exam-choice-groups">
|
||||||
|
<div>
|
||||||
|
<h2 class="h4">Erstprüfung</h2>
|
||||||
|
<div class="exam-choice-grid exam-choice-grid-primary">
|
||||||
|
<button v-for="choice in primaryChoices" :key="choice.id" type="button" class="exam-choice"
|
||||||
|
:style="{ '--exam-choice-color': choice.color }" @click="startExam(choice)">
|
||||||
|
<strong v-text="choice.shortLabel"></strong>
|
||||||
|
<span v-text="choice.label"></span>
|
||||||
|
<small v-text="partSummary(choice.parts)"></small>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="h4">Aufstockungsprüfung</h2>
|
||||||
|
<div class="exam-choice-grid">
|
||||||
|
<button v-for="choice in upgradeChoices" :key="choice.id" type="button" class="exam-choice"
|
||||||
|
:style="{ '--exam-choice-color': choice.color }" @click="startExam(choice)">
|
||||||
|
<strong v-text="choice.shortLabel"></strong>
|
||||||
|
<span v-text="choice.label"></span>
|
||||||
|
<small v-text="partSummary(choice.parts)"></small>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="exam-start-note">
|
||||||
|
<strong>So läuft die Simulation:</strong> Jeder Prüfungsteil enthält 25 Fragen. Für B, V, N und E
|
||||||
|
stehen jeweils 45 Minuten zur Verfügung, für Technik A 60 Minuten. Ein Teil ist ab 19 Punkten bestanden.
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-else-if="view === 'summary'" class="exam-summary" aria-labelledby="summary-heading">
|
||||||
|
<p class="exam-kicker">Gesamtergebnis</p>
|
||||||
|
<h1 id="summary-heading" v-text="currentChoice.label"></h1>
|
||||||
|
<div class="exam-overall" :class="overallClass">
|
||||||
|
<span class="material-icons" aria-hidden="true" v-text="overallIcon"></span>
|
||||||
|
<div>
|
||||||
|
<strong v-text="overallTitle"></strong>
|
||||||
|
<p class="mb-0" v-text="overallText"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="exam-result-list">
|
||||||
|
<article v-for="(part, index) in session.parts" :key="part.code" class="exam-result-card">
|
||||||
|
<div>
|
||||||
|
<span class="exam-result-code" v-text="part.code"></span>
|
||||||
|
<strong v-text="partDefinitions[part.code].label"></strong>
|
||||||
|
</div>
|
||||||
|
<template v-if="part.status === 'evaluated'">
|
||||||
|
<span class="exam-score"><b v-text="part.score"></b>/25</span>
|
||||||
|
<span class="exam-status" :class="resultClass(part)" v-text="resultLabel(part)"></span>
|
||||||
|
<button type="button" class="btn btn-outline-dark btn-sm" @click="reviewPart(index)">Auswertung ansehen</button>
|
||||||
|
</template>
|
||||||
|
<span v-else class="text-secondary">Nicht bearbeitet</span>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-primary" @click="newExam">Neue Probeprüfung</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-else class="exam-session" aria-labelledby="part-heading">
|
||||||
|
<header class="exam-toolbar">
|
||||||
|
<div>
|
||||||
|
<p class="exam-kicker" v-text="currentChoice.label"></p>
|
||||||
|
<h1 id="part-heading" v-text="currentPartDefinition.label"></h1>
|
||||||
|
<div class="exam-progress-label">
|
||||||
|
Prüfungsteil <span v-text="displayedPartIndex + 1"></span> von <span v-text="session.parts.length"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="exam-toolbar-actions">
|
||||||
|
<div v-if="currentPart.status === 'running'" class="exam-timer" :class="{ 'exam-timer-warning': remainingSeconds <= 300 }"
|
||||||
|
role="timer" aria-live="off">
|
||||||
|
<span class="material-icons" aria-hidden="true">timer</span>
|
||||||
|
<span v-text="formattedTime"></span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="exam-timer exam-timer-finished">
|
||||||
|
<span class="material-icons" aria-hidden="true">task_alt</span>
|
||||||
|
abgegeben
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm" @click="abortExam">Prüfung abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div v-if="currentPart.status === 'evaluated'" class="exam-part-result" :class="resultClass(currentPart)">
|
||||||
|
<span class="material-icons" aria-hidden="true" v-text="resultIcon(currentPart)"></span>
|
||||||
|
<div>
|
||||||
|
<strong><span v-text="currentPart.score"></span> von 25 Punkten – <span v-text="resultLabel(currentPart)"></span></strong>
|
||||||
|
<p class="mb-0" v-text="resultDescription(currentPart)"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="exam-paper-layout">
|
||||||
|
<aside class="exam-answer-sheet" aria-label="Antwortbogen">
|
||||||
|
<div class="answer-sheet-paper">
|
||||||
|
<h2>Antwortbogen</h2>
|
||||||
|
<div class="answer-sheet-part">
|
||||||
|
<span>Prüfungsteil</span>
|
||||||
|
<strong v-text="currentPartDefinition.formLabel"></strong>
|
||||||
|
<small><span v-text="currentPartDefinition.minutes"></span> Minuten · 25 Fragen</small>
|
||||||
|
</div>
|
||||||
|
<p class="answer-sheet-hint">Kreuze pro Frage genau eine Antwort an. Korrigierte Antworten werden wie auf dem Papierbogen mit Linien markiert.</p>
|
||||||
|
<table class="answer-table">
|
||||||
|
<thead><tr><th>Nr.</th><th v-for="label in answerLabels" :key="label" v-text="label"></th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(question, questionIndex) in currentPart.questions" :key="question.id">
|
||||||
|
<th scope="row">
|
||||||
|
<button type="button" class="answer-number" @click="scrollToQuestion(questionIndex)"
|
||||||
|
:aria-label="'Zu Frage ' + (questionIndex + 1)" v-text="padNumber(questionIndex + 1)"></button>
|
||||||
|
</th>
|
||||||
|
<td v-for="(_, answerIndex) in answerLabels" :key="answerIndex"
|
||||||
|
:class="answerCellClass(questionIndex, answerIndex)">
|
||||||
|
<button type="button" class="answer-mark" :disabled="currentPart.status !== 'running'"
|
||||||
|
@click="selectAnswer(questionIndex, answerIndex)"
|
||||||
|
:aria-label="'Frage ' + (questionIndex + 1) + ', Antwort ' + answerLabels[answerIndex]"
|
||||||
|
v-html="answerMark(questionIndex, answerIndex)"></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="answer-sheet-count"><b v-text="answeredCount"></b> von 25 beantwortet</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="exam-questions course">
|
||||||
|
<article v-for="(questionState, questionIndex) in currentPart.questions" :key="questionState.id"
|
||||||
|
class="question exam-question" :class="'layout-' + questionData(questionState).layout"
|
||||||
|
:id="'exam-question-' + questionIndex">
|
||||||
|
<div class="question-text">
|
||||||
|
<span v-text="padNumber(questionIndex + 1) + ': '"></span><span v-html="questionData(questionState).question"></span>
|
||||||
|
</div>
|
||||||
|
<div v-if="questionData(questionState).picture" class="question-picture">
|
||||||
|
<img :src="'pictures/' + questionData(questionState).picture + '.svg'" :alt="questionData(questionState).pictureAlt">
|
||||||
|
</div>
|
||||||
|
<div class="answers" data-no-shuffle>
|
||||||
|
<button v-for="(sourceIndex, answerIndex) in questionState.order" :key="answerIndex" type="button"
|
||||||
|
:disabled="currentPart.status !== 'running'" :class="questionAnswerClass(questionIndex, answerIndex)"
|
||||||
|
@click="selectAnswer(questionIndex, answerIndex)">
|
||||||
|
<b class="exam-answer-label" v-text="answerLabels[answerIndex] + ':'"></b>
|
||||||
|
<span v-if="questionData(questionState).answers[sourceIndex].text"
|
||||||
|
v-html="questionData(questionState).answers[sourceIndex].text"></span>
|
||||||
|
<span v-if="questionData(questionState).answers[sourceIndex].picture" class="question-picture">
|
||||||
|
<img :src="'pictures/' + questionData(questionState).answers[sourceIndex].picture + '.svg'"
|
||||||
|
:alt="questionData(questionState).answers[sourceIndex].pictureAlt">
|
||||||
|
</span>
|
||||||
|
<span v-if="currentPart.status === 'evaluated' && sourceIndex === 0" class="result material-icons">check</span>
|
||||||
|
<span v-else-if="currentPart.status === 'evaluated' && questionState.selected === answerIndex" class="result material-icons">clear</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="currentPart.status === 'evaluated' && questionData(questionState).hasSolution" class="solution">
|
||||||
|
<a class="btn btn-warning btn-sm d-inline-flex align-items-center" :href="questionState.id + '.html'" target="_blank"
|
||||||
|
:aria-label="'Lösungshinweis zu Frage ' + questionState.id">
|
||||||
|
<span class="material-icons exam-solution-icon" aria-hidden="true">lightbulb</span> Lösungsweg anzeigen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div class="exam-submit-panel">
|
||||||
|
<template v-if="currentPart.status === 'running'">
|
||||||
|
<p><b v-text="answeredCount"></b> von 25 Fragen beantwortet.</p>
|
||||||
|
<button type="button" class="btn btn-primary btn-lg" @click="requestSubmit">Prüfungsteil abgeben</button>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="isReviewing">
|
||||||
|
<button type="button" class="btn btn-primary" @click="view = 'summary'">Zurück zum Gesamtergebnis</button>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="hasNextPart">
|
||||||
|
<button type="button" class="btn btn-primary btn-lg" @click="startNextPart">Nächsten Prüfungsteil starten</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" @click="finishExamEarly">Prüfung hier beenden</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<button type="button" class="btn btn-primary btn-lg" @click="showSummary">Gesamtergebnis anzeigen</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
@@ -13,6 +13,9 @@
|
|||||||
<link rel="stylesheet" href="assets/style-fiftyohm.css?ts=202312172012">
|
<link rel="stylesheet" href="assets/style-fiftyohm.css?ts=202312172012">
|
||||||
<link rel="stylesheet" href="assets/style-course.css?ts=202312172012">
|
<link rel="stylesheet" href="assets/style-course.css?ts=202312172012">
|
||||||
<link rel="stylesheet" href="assets/katex/katex.min.css">
|
<link rel="stylesheet" href="assets/katex/katex.min.css">
|
||||||
|
{% for stylesheet in page_stylesheets|default([]) %}
|
||||||
|
<link rel="stylesheet" href="{{ stylesheet }}">
|
||||||
|
{% endfor %}
|
||||||
<script defer src="assets/katex/katex.min.js"></script>
|
<script defer src="assets/katex/katex.min.js"></script>
|
||||||
<script defer src="assets/katex/auto-render.min.js"></script>
|
<script defer src="assets/katex/auto-render.min.js"></script>
|
||||||
<script defer src="assets/latex.js"></script>
|
<script defer src="assets/latex.js"></script>
|
||||||
@@ -86,6 +89,12 @@
|
|||||||
Prüfung
|
Prüfung
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="probeprüfung.html">
|
||||||
|
<span class="material-icons">quiz</span>
|
||||||
|
Probeprüfung
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<!-- <li>
|
<!-- <li>
|
||||||
<a href="login.html">
|
<a href="login.html">
|
||||||
<span class="material-icons">login</span>
|
<span class="material-icons">login</span>
|
||||||
@@ -188,6 +197,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="assets/bootstrap-5.2.0-beta1-dist/js/bootstrap.bundle.min.js"></script>
|
<script src="assets/bootstrap-5.2.0-beta1-dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
{% for script in page_scripts|default([]) %}
|
||||||
|
<script src="{{ script }}"></script>
|
||||||
|
{% endfor %}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user