This commit is contained in:
sajjadtalkhabi
2026-05-21 15:51:32 +03:30
parent 8990ceaddd
commit 74227d5021
71 changed files with 2014 additions and 1666 deletions
@@ -4,12 +4,16 @@
v-for="(question, questionIndex) in questions"
:key="question.id"
class="exam-question-builder__card"
:class="{ 'exam-question-builder__card--readonly': isLocked(question) }"
>
<div class="exam-question-builder__head">
<p class="exam-question-builder__title">سوال شماره {{ questionIndex + 1 }}</p>
<p class="exam-question-builder__title">
سوال شماره {{ questionIndex + 1 }}
<span v-if="isLocked(question)" class="exam-question-builder__lock">(ذخیره شده)</span>
</p>
<button
type="button"
:disabled="disabled"
:disabled="disabled || isLocked(question)"
class="exam-question-builder__remove"
aria-label="حذف سوال"
@click="removeQuestion(question.id)"
@@ -22,24 +26,24 @@
<div class="exam-question-builder__col exam-question-builder__col--main">
<label class="exam-question-builder__label">متن سوال</label>
<textarea
:value="question.title"
:disabled="disabled"
:value="question.questionText"
:disabled="disabled || isLocked(question)"
rows="1"
placeholder="لطفا سوال خود را وارد کنید"
class="exam-question-builder__textarea"
@input="updateQuestion(question.id, { title: $event.target.value })"
@input="updateQuestion(question.id, { questionText: $event.target.value })"
/>
</div>
<div class="exam-question-builder__col exam-question-builder__col--score">
<label class="exam-question-builder__label">بارم نمره</label>
<label class="exam-question-builder__label">ترتیب</label>
<input
:value="question.score"
:disabled="disabled"
:value="question.position"
:disabled="disabled || isLocked(question)"
type="text"
inputmode="numeric"
placeholder="15"
placeholder="1"
class="exam-question-builder__input"
@input="updateQuestion(question.id, { score: $event.target.value })"
@input="updateQuestion(question.id, { position: $event.target.value })"
/>
</div>
</div>
@@ -49,31 +53,31 @@
<p class="exam-question-builder__label">گزینه‌ها</p>
<div class="exam-question-builder__answers-list">
<div
v-for="answer in question.answers"
:key="answer.id"
v-for="option in question.options"
:key="option.id"
class="exam-question-builder__answer"
>
<input
:value="answer.title"
:disabled="disabled"
:value="option.optionText"
:disabled="disabled || isLocked(question)"
type="text"
placeholder="متن گزینه"
class="exam-question-builder__answer-input"
@input="updateOption(question.id, answer.id, $event.target.value)"
@input="updateOption(question.id, option.id, $event.target.value)"
/>
<button
type="button"
:disabled="disabled"
:disabled="disabled || isLocked(question)"
class="exam-question-builder__answer-remove"
aria-label="حذف گزینه"
@click="removeOption(question.id, answer.id)"
@click="removeOption(question.id, option.id)"
>
<SvgIcon name="close" :size="14" color="#b1b1b1" />
</button>
</div>
<button
type="button"
:disabled="disabled"
:disabled="disabled || isLocked(question)"
class="exam-question-builder__answer-add"
aria-label="افزودن گزینه"
@click="addOption(question.id)"
@@ -85,16 +89,14 @@
<div class="exam-question-builder__correct">
<SelectField
:model-value="question.correctAnswerId"
:model-value="correctOptionId(question)"
:name="`correctAnswer-${question.id}`"
:options="correctAnswerOptions(question)"
:options="correctOptions(question)"
option-label="label"
option-value="value"
label="گزینه صحیح"
:disabled="disabled || question.answers.length === 0"
@update:model-value="
(value) => updateQuestion(question.id, { correctAnswerId: value || null })
"
:disabled="disabled || isLocked(question) || question.options.length === 0"
@update:model-value="(value) => setCorrectOption(question.id, value)"
/>
</div>
</div>
@@ -128,22 +130,26 @@ const defaultLabels = ['گزینه اول', 'گزینه دوم', 'گزینه س
const questions = computed(() => (Array.isArray(props.modelValue) ? props.modelValue : []))
// Backend has no PATCH/DELETE for questions/options — once a question came
// from the server (numeric id, no `__local`), the form locks editing it.
const isLocked = (question) => question?.__local !== true
const cloneQuestions = () =>
questions.value.map((q) => ({
...q,
answers: Array.isArray(q.answers) ? q.answers.map((a) => ({ ...a })) : [],
options: Array.isArray(q.options) ? q.options.map((o) => ({ ...o })) : [],
}))
const createId = (prefix) => `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`
const createOption = () => ({ id: createId('answer'), title: '' })
const createOption = () => ({ id: createId('option'), optionText: '', isCorrect: false })
const createQuestion = () => ({
id: createId('question'),
title: '',
score: '',
correctAnswerId: null,
answers: [createOption(), createOption()],
questionText: '',
position: questions.value.length + 1,
options: [createOption(), createOption()],
__local: true,
})
const emitQuestions = (next) => emit('update:modelValue', next)
@@ -163,39 +169,48 @@ const updateQuestion = (questionId, patch) => {
const addOption = (questionId) => {
const next = cloneQuestions().map((q) => {
if (q.id !== questionId) return q
return { ...q, answers: [...q.answers, createOption()] }
return { ...q, options: [...q.options, createOption()] }
})
emitQuestions(next)
}
const removeOption = (questionId, answerId) => {
const removeOption = (questionId, optionId) => {
const next = cloneQuestions().map((q) => {
if (q.id !== questionId) return q
const options = q.options.filter((o) => o.id !== optionId)
return { ...q, options }
})
emitQuestions(next)
}
const updateOption = (questionId, optionId, optionText) => {
const next = cloneQuestions().map((q) => {
if (q.id !== questionId) return q
const answers = q.answers.filter((a) => a.id !== answerId)
return {
...q,
answers,
correctAnswerId: String(q.correctAnswerId) === String(answerId) ? null : q.correctAnswerId,
options: q.options.map((o) => (o.id === optionId ? { ...o, optionText } : o)),
}
})
emitQuestions(next)
}
const updateOption = (questionId, answerId, title) => {
const setCorrectOption = (questionId, optionId) => {
const next = cloneQuestions().map((q) => {
if (q.id !== questionId) return q
return {
...q,
answers: q.answers.map((a) => (a.id === answerId ? { ...a, title } : a)),
options: q.options.map((o) => ({ ...o, isCorrect: String(o.id) === String(optionId) })),
}
})
emitQuestions(next)
}
const correctOptionId = (question) => question.options.find((o) => o.isCorrect)?.id ?? null
const optionLabel = (index) => defaultLabels[index] || `گزینه ${index + 1}`
const correctAnswerOptions = (question) =>
question.answers.map((a, idx) => ({ value: a.id, label: optionLabel(idx) }))
const correctOptions = (question) =>
question.options.map((o, idx) => ({ value: o.id, label: optionLabel(idx) }))
</script>
<style lang="scss" scoped>
@@ -209,6 +224,10 @@ const correctAnswerOptions = (question) =>
border-radius: 1.5rem;
padding: 0.875rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 2.5%);
&--readonly {
background: rgba(0, 0, 0, 2%);
}
}
&__head {
@@ -227,6 +246,12 @@ const correctAnswerOptions = (question) =>
margin: 0;
}
&__lock {
font-size: 0.7rem;
color: #9c9c9c;
margin-inline-start: 0.25rem;
}
&__remove {
width: 2rem;
height: 2rem;