| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139 |
- <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 class="photo_btn" type="primary"
- :disabled="isSingle || photoLimitReached" :loading="uploading"
- @click="handlePhoto">
- <img class="camera_icon" :src="isSingle || photoLimitReached ? cameraGray : 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, // 预览起始图片下标
- cameraGray: require('@/assets/homeworkTask/camera_999.png'),
- cameraWhite: require('@/assets/homeworkTask/camera_fff.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');
- const time = `${pad(hour)}:${pad(minute)}:${pad(second)}`;
- return day > 0 ? `${day}天 ${time}` : time;
- },
- },
- 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 2px;
- border-radius: 4px;
- color: #fff;
- font-size: 12px;
- line-height: 1;
- }
- .task_title {
- font-size: 16px;
- font-weight: 600;
- 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: 600;
- color: #333;
- padding: 13px 10px;
- border-bottom: 1px solid #EBEEF5;
- }
- .nav_group {
- padding: 10px;
- &+.nav_group {
- margin-top: 24px;
- }
- .group_name {
- font-size: 14px;
- color: #333;
- font-weight: 600;
- 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: 600;
- font-size: 14px;
- color: #333333;
- box-sizing: border-box;
- }
- .q_type {
- font-weight: 600;
- 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;
- }
- &.is-disabled,
- &.is-disabled:hover,
- &.is-disabled:focus {
- background: #F3F3F3;
- color: #C0C4CC;
- border: 1px solid #C0C4CC;
- }
- }
- .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>
|