| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129 |
- <template>
- <!-- 教辅作业作答页。路由固定为 /teachingHomework/answer,不使用地址栏参数 -->
- <div class="homework_answer" v-if="homework">
- <!-- 顶部:返回、科目、任务信息、倒计时、作答进度 -->
- <div class="answer_header">
- <el-button class="back_btn" @click="goBack">
- <i class="iconfont icon_return"></i>返回
- </el-button>
- <div class="header_main">
- <div class="title_row">
- <span class="subject_badge" :style="{ background: subjectColor }">{{ homework.subjectName }}</span>
- <span class="task_title">{{ taskTitle }}</span>
- </div>
- <div class="meta_row">
- <span>布置教师:{{ homework.teacherName }}</span>
- <span>试题数量:{{ questions.length }}</span>
- <span>结束时间:{{ homework.endTime || '-' }}</span>
- <span class="countdown">倒计时:{{ countdownText }}</span>
- <span class="progress">作答进度:{{ answeredCount }}/{{ questions.length }}</span>
- </div>
- </div>
- </div>
- <!-- 主体:左侧试题序号,右侧题干 + 底部固定作答区 -->
- <div class="answer_body" v-loading="loading">
- <!-- 试题列表:单选题 / 解答题分组,序号可切换当前题 -->
- <div class="question_nav">
- <div class="nav_title">试题列表</div>
- <div class="nav_group" v-for="group in questionGroups" :key="group.type">
- <div class="group_name">{{ group.name }}</div>
- <div class="num_grid">
- <button
- v-for="item in group.list"
- :key="item.id"
- type="button"
- class="num_btn"
- :class="numClass(item)"
- @click="selectQuestion(item.index)">
- {{ item.index + 1 }}
- </button>
- </div>
- </div>
- </div>
- <!-- 右侧题目:上方滚动题干,下方作答区不随题干滚动 -->
- <div class="question_panel" v-if="currentQuestion">
- <div class="question_scroll">
- <div class="q_tags">
- <span class="q_no">{{ currentQuestion.index + 1 }}</span>
- <span class="q_type">{{ currentQuestion.typeName }}</span>
- <span class="q_level">{{ currentQuestion.difficulty }}</span>
- </div>
- <div class="q_stem" v-html="currentQuestion.stem"></div>
- <div v-if="currentQuestion.type === 'single' && hasOptionText" class="q_options">
- <div class="q_option" v-for="opt in currentQuestion.options" :key="opt.key">
- <span class="opt_key">{{ opt.key }}.</span>
- <span>{{ opt.text }}</span>
- </div>
- </div>
- </div>
- <!-- 作答区固定在题目卡片底部。选择题选选项,解答题只拍照,没有手写输入框 -->
- <div class="answer_dock">
- <!-- 选择题:我的作答 + 选项;解答题:已上传图片,可删除 -->
- <div class="dock_answer">
- <span class="dock_label">我的作答:</span>
- <!-- <span v-if="isSingle" class="dock_sub_no">(1)</span> -->
- <el-radio-group v-if="isJudge" class="choice_radio" v-model="answers[currentQuestion.id]"
- @change="persistState">
- <el-radio label="T">对</el-radio>
- <el-radio label="F">错</el-radio>
- </el-radio-group>
- <el-checkbox-group v-else-if="isSingle" class="choice_radio" v-model="answers[currentQuestion.id]"
- @change="persistState">
- <el-checkbox v-for="opt in currentQuestion.options" :key="opt.key" :label="opt.key">
- {{ opt.key }}
- </el-checkbox>
- </el-checkbox-group>
- <div v-else class="photo_list">
- <div
- class="photo_item"
- v-for="(img, imgIndex) in currentPhotos"
- :key="imgIndex"
- :class="{ active: imgIndex === 0 }">
- <img :src="img" alt="" @click="previewPhoto(imgIndex)">
- <i class="el-icon-close photo_remove" @click.stop="removePhoto(imgIndex)"></i>
- </div>
- </div>
- </div>
- <!-- 判断题、单选题、多选题不显示拍照;解答题可上传。最后一题把「下一题」换成「提交」 -->
- <div class="dock_actions">
- <el-button v-if="!isSingle" class="photo_btn" type="primary"
- :disabled="photoLimitReached" :loading="uploading"
- @click="handlePhoto">
- <img class="camera_icon" :src="cameraWhite" alt="">
- 拍照上传
- </el-button>
- <el-button class="nav_btn" :disabled="currentQuestion.index === 0" @click="prevQuestion">
- 上一题
- </el-button>
- <el-button v-if="!isLastQuestion" class="nav_btn" :loading="saving" @click="nextQuestion">
- 下一题
- </el-button>
- <el-button v-else class="nav_btn" :loading="saving" @click="submitAnswer">
- 提交
- </el-button>
- </div>
- <input ref="photoInput" class="photo_input" type="file" accept="image/*" multiple @change="onPhotoChange">
- </div>
- </div>
- </div>
- <!-- 图片放大预览:支持缩放、旋转,复用 Element UI ImageViewer -->
- <el-image-viewer
- v-if="imgVisible"
- :z-index="9999"
- :initial-index="previewIndex"
- :url-list="currentPhotos"
- :on-close="closePhotoPreview" />
- </div>
- </template>
- <script>
- import ElImageViewer from 'element-ui/packages/image/src/image-viewer';
- // 作答状态只存 sessionStorage,刷新仍停留本页,且地址栏不带参数
- const STORAGE_KEY = 'teachingHomeworkAnswer';
- const OPTION_KEYS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
- // 每道主观题最多上传的作答图片数量
- const MAX_PHOTOS = 4;
- function isChoiceQuestion(item) {
- return Number(item.isObjective) === 1 || Number(item.isJudge) === 1;
- }
- function buildOptions(item) {
- const count = Number(item.optionsNum) || (Number(item.isJudge) === 1 ? 2 : 4);
- const options = [];
- for (let i = 0; i < count && i < OPTION_KEYS.length; i += 1) {
- options.push({ key: OPTION_KEYS.charAt(i), text: '' });
- }
- return options;
- }
- function mapQuestion(item, index) {
- const judge = Number(item.isJudge) === 1;
- const choice = judge || isChoiceQuestion(item);
- const id = item.homeWorkStuQuesId || item.questionId || index;
- return {
- id,
- questionId: item.questionId,
- homeWorkStuQuesId: item.homeWorkStuQuesId,
- isJudge: judge,
- type: choice ? 'single' : 'photo',
- typeName: item.questionTypeName || (judge ? '判断题' : (choice ? '客观题' : '主观题')),
- groupName: item.questionTypeName || (judge ? '判断题' : (choice ? '客观题' : '主观题')),
- difficulty: item.difficultyName || '',
- stem: item.questionContent || '',
- options: judge ? [] : (choice ? buildOptions(item) : []),
- writeAnswer: item.writeAnswer || '',
- writeUrls: Array.isArray(item.writeUrls) ? item.writeUrls : [],
- submitEndTime: item.submitEndTime || '',
- };
- }
- export default {
- name: 'TeachingHomeworkAnswer',
- components: {
- ElImageViewer,
- },
- data() {
- return {
- homework: null, // 当前作业任务,来自 sessionStorage
- questions: [],
- loading: false,
- currentIndex: 0, // 当前题下标
- answers: {}, // 选择题为所选选项 key 数组,拍照题为图片 url 数组
- imgVisible: false, // 是否打开图片放大预览
- previewIndex: 0, // 预览起始图片下标
- cameraWhite: require('@/assets/homeworkTask/camera_999.png'),
- nowTick: Date.now(),
- timer: null,
- uploading: false,
- saving: false,
- };
- },
- computed: {
- // 标题后拼接班级,名称里已有班级则不重复加
- taskTitle() {
- if (!this.homework) return '';
- const name = this.homework.homeworkName || '';
- const className = this.homework.className || '';
- if (!className || name.indexOf(className) !== -1) return name;
- return `${name} ${className}`;
- },
- // 科目色标,与靶向提升科目颜色一致
- subjectColor() {
- const name = (this.homework && this.homework.subjectName) || '';
- return this.$global.getSubjectsName(name, null, 'color');
- },
- // 按接口返回的 questionTypeName 分组,有几种题型就显示几组
- questionGroups() {
- const groups = [];
- this.questions.forEach((item, index) => {
- const name = item.groupName || item.typeName || '其他';
- let group = groups.find(g => g.name === name);
- if (!group) {
- group = { type: name, name, list: [] };
- groups.push(group);
- }
- group.list.push({ ...item, index });
- });
- return groups;
- },
- // 当前正在作答的题目
- currentQuestion() {
- const item = this.questions[this.currentIndex];
- if (!item) return null;
- return { ...item, index: this.currentIndex };
- },
- // 当前题是否为选择题(含判断题)
- isSingle() {
- return !!(this.currentQuestion && this.currentQuestion.type === 'single');
- },
- // isJudge 为 1 时是判断题,只显示对 / 错
- isJudge() {
- return !!(this.currentQuestion && this.currentQuestion.isJudge);
- },
- hasOptionText() {
- const options = (this.currentQuestion && this.currentQuestion.options) || [];
- return options.some(opt => opt.text);
- },
- // 是否最后一题,最后一题显示「提交」
- isLastQuestion() {
- return this.currentIndex >= this.questions.length - 1;
- },
- // 当前拍照题已上传的图片
- currentPhotos() {
- if (!this.currentQuestion || this.isSingle) return [];
- const val = this.answers[this.currentQuestion.id];
- return Array.isArray(val) ? val : [];
- },
- // 当前题是否已达到图片上传上限
- photoLimitReached() {
- return this.currentPhotos.length >= MAX_PHOTOS;
- },
- // 已作答题数,用于顶部进度
- answeredCount() {
- return this.questions.filter(item => this.isAnswered(item.id)).length;
- },
- // 倒计时:截止日期减去当前时间,按天、时、分、秒显示,并每秒刷新
- countdownText() {
- const endTime = this.homework && this.homework.endTime;
- if (!endTime) return '-';
- const text = String(endTime).trim();
- const end = new Date(text.replace(/-/g, '/'));
- if (!Number.isFinite(end.getTime())) return '-';
- if (/^\d{4}-\d{2}-\d{2}$/.test(text)) {
- end.setHours(23, 59, 59, 0);
- }
- const diff = Math.max(0, end.getTime() - this.nowTick);
- const day = Math.floor(diff / 86400000);
- const hour = Math.floor((diff % 86400000) / 3600000);
- const minute = Math.floor((diff % 3600000) / 60000);
- const second = Math.floor((diff % 60000) / 1000);
- const pad = value => String(value).padStart(2, '0');
- return `${day}天 ${pad(hour)}:${pad(minute)}:${pad(second)}`;
- },
- },
- created() {
- this.restoreState();
- this.startTimer();
- },
- beforeDestroy() {
- this.clearTimer();
- this.persistState();
- },
- methods: {
- // 进入页面或刷新时恢复作答;没有作业记录则回到列表
- restoreState() {
- let cache = null;
- try {
- cache = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || 'null');
- } catch (e) {
- cache = null;
- }
- if (!cache || !cache.homework) {
- this.$router.replace('/teachingHomework/list');
- return;
- }
- this.homework = cache.homework;
- this.currentIndex = Number(cache.currentIndex) || 0;
- this.savedAnswers = cache.answers && typeof cache.answers === 'object' ? cache.answers : {};
- this.fetchDetail();
- },
- fetchDetail() {
- const homework = this.homework || {};
- if (!homework.homeWorkStuId && !homework.homeWorkId) {
- this.questions = [];
- return;
- }
- this.loading = true;
- const params = {
- homeWorkStuId: homework.homeWorkStuId,
- homeWorkId: homework.homeWorkId,
- };
- this.$api.teachingHomework.viewHomeWorkWriteDetail(params).then(res => {
- this.applyDetail(res);
- }).catch(err => {
- const body = this.pickBody(err);
- if (body) {
- this.applyDetail(body);
- return;
- }
- this.questions = [];
- }).finally(() => {
- this.loading = false;
- });
- },
- pickBody(payload) {
- if (!payload) return null;
- if (payload.code === 0 || payload.code === 200) return payload;
- if (payload.data && (payload.data.code === 0 || payload.data.code === 200)) return payload.data;
- if (payload.response) return this.pickBody(payload.response);
- return null;
- },
- applyDetail(res) {
- const detail = this.pickDetail(res);
- const list = detail.questionDetails;
- this.mergeHomework(detail);
- this.questions = list.map(mapQuestion);
- const endTime = detail.submitEndTime
- || (this.questions[0] && this.questions[0].submitEndTime)
- || (this.homework && this.homework.endTime);
- if (endTime && this.homework) this.homework.endTime = endTime;
- this.fillAnswers();
- if (this.currentIndex < 0 || this.currentIndex >= this.questions.length) {
- this.currentIndex = 0;
- }
- this.persistState();
- },
- // 详情 data 为作业对象,题目在 questionDetails;兼容旧的题目数组
- pickDetail(res) {
- const data = res && res.data;
- if (Array.isArray(data)) return { questionDetails: data };
- if (data && typeof data === 'object') {
- return {
- ...data,
- questionDetails: Array.isArray(data.questionDetails) ? data.questionDetails : [],
- };
- }
- return { questionDetails: [] };
- },
- // 用详情里的作业信息补齐头部:名称、教师、科目、截止时间
- mergeHomework(detail) {
- const current = this.homework || {};
- this.homework = {
- ...current,
- homeWorkId: detail.homeWorkId != null ? detail.homeWorkId : current.homeWorkId,
- homeWorkStuId: detail.homeWorkStuId != null ? detail.homeWorkStuId : current.homeWorkStuId,
- homeworkName: detail.homeWorkName || current.homeworkName || '',
- teacherName: detail.createUserName || current.teacherName || '',
- subjectName: detail.courseName || current.subjectName || '',
- courseCode: detail.courseCode != null ? detail.courseCode : current.courseCode,
- endTime: detail.submitEndTime || current.endTime || '',
- questionNum: detail.questionNumber != null ? detail.questionNumber : current.questionNum,
- };
- },
- fillAnswers() {
- const savedAnswers = this.savedAnswers || {};
- const answers = {};
- this.questions.forEach(item => {
- const saved = savedAnswers[item.id] != null ? savedAnswers[item.id] : savedAnswers[String(item.id)];
- const hasSaved = savedAnswers[item.id] != null || savedAnswers[String(item.id)] != null;
- if (item.type === 'photo') {
- answers[item.id] = hasSaved && Array.isArray(saved) ? saved : item.writeUrls.slice();
- } else if (item.isJudge) {
- answers[item.id] = this.normalizeJudgeAnswer(hasSaved ? saved : item.writeAnswer);
- } else {
- // 选择题多选,答案统一为选项 key 数组;兼容历史字符串缓存与接口回显
- answers[item.id] = this.normalizeChoiceAnswer(hasSaved ? saved : item.writeAnswer, item.options);
- }
- });
- this.answers = answers;
- },
- // 判断题回显:T 为对,F 为错
- normalizeJudgeAnswer(value) {
- const text = String(value == null ? '' : value).trim();
- if (text === 'T' || text === 't' || text === '对' || text.toLowerCase() === 'true') return 'T';
- if (text === 'F' || text === 'f' || text === '错' || text.toLowerCase() === 'false') return 'F';
- return '';
- },
- // 把接口/缓存里的答案归一化为合法选项 key 数组,兼容 "A,C"、"AC"、"A|C"、["A"] 等格式
- normalizeChoiceAnswer(value, options) {
- const validKeys = (options || []).map(opt => opt.key);
- let keys = [];
- if (Array.isArray(value)) {
- keys = value;
- } else if (value !== null && value !== undefined && String(value).trim() !== '') {
- keys = String(value).toUpperCase().match(/[A-Z]/g) || [];
- }
- const result = [];
- keys.forEach(key => {
- const k = String(key);
- if (validKeys.indexOf(k) !== -1 && result.indexOf(k) === -1) result.push(k);
- });
- return result;
- },
- // 保存作业、当前题和答案,保证刷新不丢失
- persistState() {
- if (!this.homework) return;
- sessionStorage.setItem(STORAGE_KEY, JSON.stringify({
- homework: this.homework,
- currentIndex: this.currentIndex,
- answers: this.answers,
- }));
- },
- startTimer() {
- this.clearTimer();
- this.timer = setInterval(() => {
- this.nowTick = Date.now();
- }, 1000);
- },
- clearTimer() {
- if (this.timer) {
- clearInterval(this.timer);
- this.timer = null;
- }
- },
- // 选择题有选项、拍照题有图片,即视为已作答
- isAnswered(id) {
- const val = this.answers[id];
- if (Array.isArray(val)) return val.length > 0;
- return val !== undefined && val !== null && String(val).trim() !== '';
- },
- // 序号样式:当前题蓝底,已作答绿框,未作答蓝框
- numClass(item) {
- const answered = this.isAnswered(item.id);
- return {
- current: item.index === this.currentIndex,
- done: answered && item.index !== this.currentIndex,
- };
- },
- // 切题:当前题有作答才保存,没作答或为空则直接切换、不调接口
- selectQuestion(index) {
- if (index === this.currentIndex) return;
- this.goToQuestion(index);
- },
- prevQuestion() {
- if (this.currentIndex <= 0) return;
- this.goToQuestion(this.currentIndex - 1);
- },
- nextQuestion() {
- if (this.currentIndex >= this.questions.length - 1) return;
- this.goToQuestion(this.currentIndex + 1);
- },
- goToQuestion(index) {
- if (index === this.currentIndex) return;
- if (this.uploading) {
- this.$message.warning('图片正在上传,请稍候');
- return;
- }
- const jump = () => {
- this.currentIndex = index;
- this.persistState();
- };
- const question = this.currentQuestion;
- if (!question || !this.isAnswered(question.id)) {
- jump();
- return;
- }
- this.saveCurrentAnswer(0).then(ok => {
- if (!ok) return;
- jump();
- });
- },
- // 选择题不可拍照;解答题达到上限或上传中也不可再选
- handlePhoto() {
- if (this.isSingle || this.uploading) return;
- if (this.currentPhotos.length >= MAX_PHOTOS) {
- this.$message.warning(`最多上传${MAX_PHOTOS}张图片`);
- return;
- }
- const input = this.$refs.photoInput;
- if (input) input.click();
- },
- // 选择图片后校验剩余空位:超出上限整体拒绝不上传;合法数量内逐张顺序上传,成功一张追加一张
- async onPhotoChange(e) {
- const files = Array.from(e.target.files || []);
- e.target.value = '';
- if (!files.length || !this.currentQuestion || this.isSingle) return;
- // 本次选择数量超过剩余空位时整体拒绝:不上传、不渲染,请用户重新选择
- const remaining = MAX_PHOTOS - this.currentPhotos.length;
- if (remaining <= 0) {
- this.$message.warning(`最多上传${MAX_PHOTOS}张图片`);
- return;
- }
- if (files.length > remaining) {
- this.$message.warning(`最多上传${MAX_PHOTOS}张图片,当前还可上传${remaining}张,请重新选择`);
- return;
- }
- this.uploading = true;
- let failed = 0;
- for (const file of files) {
- const url = await this.uploadOnePhoto(file);
- if (url) {
- this.appendPhotoUrl(url);
- } else {
- failed += 1;
- }
- }
- this.uploading = false;
- if (failed) {
- this.$message.warning(failed === files.length ? '图片上传失败,请重试' : `有${failed}张图片上传失败,请重试`);
- }
- },
- // 上传单个图片文件,成功返回地址,失败返回空字符串
- uploadOnePhoto(file) {
- const formData = new FormData();
- formData.append('file', file);
- return this.$api.teachingHomework.uploadFilesSelect(formData).then(res => {
- return this.pickFileUrl(res);
- }).catch(err => {
- const body = this.pickBody(err);
- return body ? this.pickFileUrl(body) : '';
- });
- },
- pickFileUrl(body) {
- const data = body && body.data;
- if (!data) return '';
- if (typeof data === 'string') return data;
- if (Array.isArray(data)) {
- const first = data[0];
- if (typeof first === 'string') return first;
- return (first && (first.url || first.fileUrl || first.path || first.filePath)) || '';
- }
- return data.url || data.fileUrl || data.path || data.filePath || '';
- },
- appendPhotoUrl(url) {
- if (!url || !this.currentQuestion) {
- this.$message.warning('图片上传失败');
- return;
- }
- const id = this.currentQuestion.id;
- const list = Array.isArray(this.answers[id]) ? this.answers[id].slice() : [];
- list.push(url);
- this.$set(this.answers, id, list);
- this.persistState();
- },
- // 删除当前拍照题中的一张图片
- removePhoto(index) {
- if (!this.currentQuestion) return;
- const id = this.currentQuestion.id;
- const list = Array.isArray(this.answers[id]) ? this.answers[id].slice() : [];
- list.splice(index, 1);
- this.$set(this.answers, id, list);
- this.persistState();
- },
- // 点击缩略图放大预览(可缩放、旋转)
- previewPhoto(index) {
- this.previewIndex = index;
- this.imgVisible = true;
- },
- closePhotoPreview() {
- this.imgVisible = false;
- },
- // 保存当前题;空作答不走这里
- saveCurrentAnswer(commitFlag) {
- const question = this.currentQuestion;
- if (!question || this.saving || this.uploading) {
- if (this.uploading) this.$message.warning('图片正在上传,请稍候');
- return Promise.resolve(false);
- }
- const isJudge = !!question.isJudge;
- const isChoice = question.type === 'single' && !isJudge;
- const judgeValue = this.answers[question.id];
- const payload = {
- homeWorkId: this.homework && this.homework.homeWorkId,
- homeWorkStuId: this.homework && this.homework.homeWorkStuId,
- homeWorkStuQuesId: question.homeWorkStuQuesId,
- questionId: question.questionId,
- commitFlag,
- // 判断题传 T / F;选择题多选直接拼接,如 AB;拍照题提交图片地址
- writeAnswer: isJudge
- ? (judgeValue === 'T' || judgeValue === 'F' ? judgeValue : '')
- : (isChoice ? (this.answers[question.id] || []).join('') : ''),
- writeUrls: isJudge || isChoice ? [] : (this.answers[question.id] || []),
- };
- this.saving = true;
- return this.$api.teachingHomework.commitHomeWorkWriteAnswer(payload).then(res => {
- if (res && res.code != null && res.code !== 0 && res.code !== 200) {
- this.$message.warning(res.msg || '保存失败');
- return false;
- }
- return true;
- }).catch(err => {
- const body = this.pickBody(err);
- if (body) return true;
- this.$message.warning((err && err.data && err.data.msg) || '保存失败');
- return false;
- }).finally(() => {
- this.saving = false;
- });
- },
- // 提交时检查全部题目,有未作答则提示并跳到第一道未作答;全部作答完再提交
- submitAnswer() {
- if (this.uploading) {
- this.$message.warning('图片正在上传,请稍候');
- return;
- }
- const unanswered = this.questions
- .map((item, index) => ({ ...item, index }))
- .filter(item => !this.isAnswered(item.id));
- if (unanswered.length) {
- const nums = unanswered.map(item => item.index + 1).join('、');
- this.$message.warning(`第${nums}题没有上传答案,请作答`);
- this.currentIndex = unanswered[0].index;
- this.persistState();
- return;
- }
- this.saveCurrentAnswer(1).then(ok => {
- if (!ok) return;
- this.persistState();
- this.$message.success('提交成功');
- this.$router.push('/teachingHomework/list');
- });
- },
- // 返回列表,作答进度仍保留在 sessionStorage
- goBack() {
- this.persistState();
- this.$router.push('/teachingHomework/list');
- },
- },
- };
- </script>
- <style lang="scss" scoped>
- /* 整页纵向:顶栏固定,下方主体占满剩余高度 */
- .homework_answer {
- display: flex;
- flex-direction: column;
- width: 100%;
- height: 100%;
- min-width: 0;
- min-height: 0;
- box-sizing: border-box;
- overflow: hidden;
- .answer_header {
- flex-shrink: 0;
- display: flex;
- align-items: flex-start;
- gap: 16px;
- padding: 10px;
- background: #fff;
- border-radius: 10px;
- box-sizing: border-box;
- .back_btn {
- flex-shrink: 0;
- height: 36px;
- padding: 0 12px;
- background: #fff;
- border: 1px solid #DCDFE6;
- color: #666;
- :deep(span) {
- display: inline-flex;
- align-items: center;
- gap: 2px;
- }
- }
- .header_main {
- flex: 1;
- min-width: 0;
- .title_row {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-bottom: 10px;
- margin-top: 6px;
- .subject_badge {
- flex-shrink: 0;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- min-width: 40px;
- height: 25px;
- padding: 0 4px;
- border-radius: 4px;
- color: #fff;
- font-size: 12px;
- line-height: 1;
- }
- .task_title {
- font-size: 16px;
- font-weight: 550;
- color: #333;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- }
- .meta_row {
- display: flex;
- flex-wrap: wrap;
- gap: 16px;
- font-size: 14px;
- color: #999;
- .countdown {
- color: #F56C6C;
- }
- .progress {
- color: #2E64FA;
- }
- }
- }
- }
- /* 左导航 + 右题目 */
- .answer_body {
- flex: 1;
- min-height: 0;
- display: flex;
- gap: 10px;
- margin-top: 10px;
- overflow: hidden;
- .question_nav {
- flex-shrink: 0;
- width: 240px;
- height: 100%;
- background: #fff;
- border-radius: 10px;
- box-sizing: border-box;
- overflow-y: auto;
- .nav_title {
- font-size: 14px;
- font-weight: 550;
- color: #333;
- padding: 10px;
- border-bottom: 1px solid #EBEEF5;
- }
- .nav_group {
- padding: 10px;
- &+.nav_group {
- margin-top: 24px;
- }
- .group_name {
- font-size: 14px;
- color: #333;
- font-weight: 550;
- padding-bottom: 12px;
- }
- .num_grid {
- display: grid;
- grid-template-columns: repeat(5, 32px);
- gap: 15px;
- /* 未作答蓝框,已作答绿框,当前题蓝底白字;点击可跳题 */
- .num_btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 32px;
- height: 32px;
- padding: 0;
- border-radius: 4px;
- border: 1px solid #2E64FA;
- box-sizing: border-box;
- background: #fff;
- color: #2E64FA;
- font-size: 14px;
- cursor: pointer;
- &.done {
- border-color: #3BA272;
- color: #3BA272;
- }
- &.current {
- background: #2E64FA;
- border-color: #2E64FA;
- color: #fff;
- }
- }
- }
- }
- }
- .question_panel {
- flex: 1;
- min-width: 0;
- height: 100%;
- display: flex;
- flex-direction: column;
- background: #fff;
- border-radius: 10px;
- overflow: hidden;
- .question_scroll {
- flex: 1;
- min-height: 0;
- overflow-y: auto;
- padding: 16px;
- box-sizing: border-box;
- .q_tags {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 16px;
- .q_no {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- min-width: 24px;
- height: 24px;
- padding: 0 6px;
- background: #F3F3F3;
- border-radius: 2px;
- font-weight: 550;
- font-size: 14px;
- color: #333333;
- box-sizing: border-box;
- }
- .q_type {
- font-weight: 550;
- font-size: 14px;
- color: #333333;
- }
- .q_level {
- font-weight: 400;
- font-size: 14px;
- color: #90CB75;
- margin-left: 8px;
- }
- }
- .q_stem {
- font-size: 15px;
- line-height: 1.7;
- color: #333;
- margin-bottom: 16px;
- :deep(img) {
- max-width: 100%;
- }
- }
- .q_options {
- .q_option {
- display: flex;
- gap: 6px;
- margin-bottom: 10px;
- font-size: 14px;
- color: #333;
- line-height: 1.6;
- .opt_key {
- flex-shrink: 0;
- }
- }
- }
- }
- /* 作答区贴在题目卡片底部,不随题干滚动 */
- .answer_dock {
- flex-shrink: 0;
- display: flex;
- flex-direction: column;
- align-items: stretch;
- gap: 16px;
- padding: 0 16px 16px 16px;
- background: #fff;
- box-sizing: border-box;
- .dock_answer {
- display: flex;
- align-items: center;
- gap: 16px;
- min-height: 56px;
- padding: 10px 16px;
- background: #F5F7FA;
- border-radius: 10px;
- box-sizing: border-box;
- border: 1px solid #DCDFE6;
- .dock_label {
- flex-shrink: 0;
- display: inline-flex;
- align-items: center;
- height: 20px;
- line-height: 20px;
- font-size: 14px;
- color: #333;
- }
- .dock_sub_no {
- flex-shrink: 0;
- display: inline-flex;
- align-items: center;
- height: 20px;
- line-height: 20px;
- font-size: 14px;
- color: #333;
- }
- .choice_radio {
- display: inline-flex;
- align-items: center;
- min-height: 20px;
- gap: 20px;
- :deep(.el-checkbox) {
- margin-right: 0 !important;
- height: 20px;
- display: inline-flex !important;
- align-items: center;
- }
- :deep(.el-checkbox__input) {
- display: inline-flex;
- align-items: center;
- height: 16px;
- line-height: 16px;
- }
- :deep(.el-checkbox__inner) {
- width: 16px;
- height: 16px;
- border: 1px solid #DCDFE6;
- background: #fff;
- border-radius: 2px;
- &::after {
- top: 1px;
- left: 5px;
- width: 3px;
- height: 8px;
- }
- }
- :deep(.el-checkbox__input.is-focus .el-checkbox__inner) {
- background-color: #fff;
- border-color: #2E64FA;
- }
- :deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
- background-color: #2E64FA;
- border-color: #2E64FA;
- }
- :deep(.el-checkbox__input.is-checked + .el-checkbox__label) {
- color: #333;
- }
- :deep(.el-checkbox__label) {
- display: inline-flex;
- align-items: center;
- height: 20px;
- padding-left: 8px;
- font-size: 14px;
- color: #333;
- line-height: 20px;
- }
- :deep(.el-radio) {
- margin-right: 0 !important;
- height: 20px;
- display: inline-flex !important;
- align-items: center;
- }
- :deep(.el-radio__input) {
- display: inline-flex;
- align-items: center;
- height: 16px;
- line-height: 16px;
- }
- :deep(.el-radio__inner) {
- width: 16px;
- height: 16px;
- border: 1px solid #DCDFE6;
- background: #fff;
- }
- :deep(.el-radio__input.is-checked .el-radio__inner) {
- background-color: #fff;
- border: 1px solid #2E64FA;
- box-shadow: none;
- }
- :deep(.el-radio__input.is-checked .el-radio__inner::after) {
- width: 10px;
- height: 10px;
- background-color: #2E64FA;
- transform: translate(-50%, -50%) scale(1);
- }
- :deep(.el-radio__input.is-checked + .el-radio__label),
- :deep(.el-radio__label) {
- padding-left: 8px;
- font-size: 14px;
- color: #333;
- line-height: 20px;
- }
- }
- .photo_list {
- display: flex;
- flex-wrap: wrap;
- gap: 10px;
- min-width: 0;
- .photo_item {
- position: relative;
- width: 64px;
- height: 64px;
- border-radius: 4px;
- border: 1px solid transparent;
- box-sizing: border-box;
- background: #fff;
- &.active {
- border-color: #2E64FA;
- }
- img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- border-radius: 4px;
- display: block;
- cursor: zoom-in;
- }
- .photo_remove {
- position: absolute;
- top: -6px;
- right: -6px;
- width: 16px;
- height: 16px;
- border-radius: 50%;
- background: #C0C4CC;
- color: #fff;
- font-size: 12px;
- line-height: 16px;
- text-align: center;
- cursor: pointer;
- }
- }
- }
- }
- .photo_input {
- display: none;
- }
- .dock_actions {
- display: flex;
- align-items: center;
- gap: 16px;
- .el-button {
- height: 36px;
- padding: 0 14px;
- border-radius: 4px;
- line-height: 34px;
- margin:0;
- &.is-disabled,
- &.is-disabled:hover,
- &.is-disabled:focus {
- background: #F3F3F3;
- color: #C0C4CC;
- border: 1px solid #C0C4CC;
- }
- }
- .photo_btn {
- :deep(span) {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- }
- .camera_icon {
- width: 16px;
- height: 16px;
- display: block;
- }
- }
- .nav_btn {
- color: #2E64FA;
- border-color: #2E64FA;
- background: #fff;
- &.is-disabled,
- &.is-disabled:hover,
- &.is-disabled:focus {
- background: #F3F3F3;
- color: #C0C4CC;
- border: 1px solid #C0C4CC;
- }
- }
- }
- }
- }
- }
- }
- </style>
|