Bläddra i källkod

学生端-新增教辅作业。

吴朋磊 3 veckor sedan
förälder
incheckning
17c1175f47

+ 27 - 0
src/http/api/teachingHomework.js

@@ -9,6 +9,33 @@ const teachingHomework = {
   queryClassHomeWorkByPage(data) {
     return get(base.prefix + `/api/v1/student_class_home_work/query_class_home_work_by_page`, data);
   },
+  // 作业任务详情
+  viewHomeWorkWriteDetail(data) {
+    return get(base.prefix + `/api/v1/student_class_home_work/view_home_work_write_detail`, data);
+  },
+  // 作业任务上传图片文件地址
+  uploadFilesSelect(data) {
+    return post(base.prefix + `/api/v1/student_upload/oss/upload_filesSele`, data);
+  },
+  // 作业任务-点击下一题保存
+  commitHomeWorkWriteAnswer(data) {
+    return post(base.prefix + `/api/v1/student_class_home_work/commit_home_work_write_answer`, data);
+  },
+  // 作业任务-查看分析
+  getStudentAnalysis(data) {
+    return get(base.prefix + `/api/v1/student_analysis_home_work/getStudentAnalysis`, data);
+  },
+  // 作业任务-导出分析
+  // params: { fileName 文件名, sheetName sheet页名称, headDataList 表头数据(按行二维数组), dataList 数据(按行二维数组) }
+  exportStudentAnalysis(data) {
+    return post(
+      base.prefix + `/api/v1/student_analysis_home_work/excel/export/publicStudentExportHeader`,
+      data,
+      false,
+      { responseType: 'blob' }
+    );
+  },
 };
 
 export default teachingHomework;
+

+ 809 - 312
src/views/teachingHomework/homeworkTask/analysis.vue

@@ -14,7 +14,7 @@
       </div>
     </div>
 
-    <div class="analysis_body">
+    <div class="analysis_body" v-loading="loading">
       <div class="side_nav">
         <button
           v-for="item in navList"
@@ -40,7 +40,7 @@
           </div>
         </section>
 
-        <!-- 小题分析图 + 选中题详情 -->
+        <!-- 小题分析图(独立卡片) -->
         <section class="card" ref="qChart">
           <div class="card_head">
             <div class="card_title">小题分析图</div>
@@ -55,99 +55,189 @@
               </span>
             </div>
           </div>
-          <BarChart
-            v-if="qChartType === 'vertical_bar'"
-            :datax="questionDatax"
-            :datay="questionDatay"
-            typeName="得分率"
-            unit="%"
-            :gridLeft="0"
-            :gridRight="0"
-            :isClick="true"
-            @HandleChartClick="onQuestionBarClick" />
-          <RadarCharts
-            v-else
-            :data="questionRadarData"
-            :showCheckBox="false"
-            :showLegend="false" />
-
-          <div class="question_detail" v-if="selectedQuestion">
-            <div class="detail_left">
-              <div class="detail_head">
-                <span class="detail_title">{{ selectedQuestion.name }} 题干</span>
-                <el-button class="ai_btn" @click="handleAiExplain">AI讲解</el-button>
-              </div>
-              <div class="knowledge_tags">
-                <span class="knowledge_tag">知识点:{{ selectedQuestion.knowledge }}</span>
-                <span
-                  class="knowledge_tag"
-                  v-for="(tag, idx) in selectedQuestion.extraTags"
-                  :key="idx">
-                  {{ tag }}
-                </span>
-              </div>
-              <div class="stem_text">{{ selectedQuestion.stem }}</div>
-              <div class="option_list" v-if="selectedQuestion.options.length">
-                <div class="option_item" v-for="opt in selectedQuestion.options" :key="opt.key">
-                  {{ opt.key }}. {{ opt.text }}
+          <template v-if="questionRows.length">
+            <BarChart
+              v-if="qChartType === 'vertical_bar'"
+              :datax="questionDatax"
+              :datay="questionDatay"
+              typeName="得分率"
+              unit="%"
+              :gridLeft="0"
+              :gridRight="0"
+              :isClick="true"
+              @HandleChartClick="onQuestionBarClick" />
+            <RadarCharts
+              v-else
+              :data="questionRadarData"
+              :showCheckBox="false"
+              :showLegend="false" />
+          </template>
+          <div v-else-if="!loading" class="empty_tip no_content_data">
+            <span>暂无数据</span>
+          </div>
+        </section>
+
+        <!-- 选中小题:左题干 / 右我的作答,两张独立卡片 -->
+        <div class="question_detail_wrap" v-if="selectedQuestion">
+          <div class="card detail_card stem_card">
+            <div class="detail_head">
+              <span class="detail_title">第{{ selectedIndex + 1 }}题 {{ selectedQuestion.name }}</span>
+              <el-button class="ai_btn" @click="handleAiExplain">AI讲题</el-button>
+            </div>
+            <div class="knowledge_tags" v-if="selectedQuestion.knowledgeTags.length">
+              <span class="knowledge_label">知识点:</span>
+              <span
+                class="knowledge_tag"
+                v-for="(tag, idx) in selectedQuestion.knowledgeTags"
+                :key="idx"
+                :title="tag">
+                {{ formatKnowledgeTag(tag) }}
+              </span>
+            </div>
+
+            <div class="stem_scroll">
+              <!-- 大题题干 -->
+              <template v-if="selectedQuestion.title || selectedQuestion.options.length">
+                <div class="stem_text" v-if="selectedQuestion.title" v-html="selectedQuestion.title"></div>
+                <div class="option_list" v-if="selectedQuestion.options.length">
+                  <div class="option_item" v-for="opt in selectedQuestion.options" :key="opt.key">
+                    <span class="opt_key">{{ opt.key }}.</span>
+                    <span class="opt_text" v-html="opt.text"></span>
+                  </div>
+                </div>
+              </template>
+
+              <!-- 子题 -->
+              <div
+                class="sub_question"
+                v-for="(sub, sIdx) in selectedQuestion.subQuestions"
+                :key="sub.id || sIdx">
+                <div class="sub_title" v-if="sub.title" v-html="sub.title"></div>
+                <!-- <div class="knowledge_tags" v-if="sub.knowledgeTags.length">
+                  <span
+                    class="knowledge_tag sm"
+                    v-for="(tag, kIdx) in sub.knowledgeTags"
+                    :key="kIdx"
+                    :title="tag">
+                    {{ formatKnowledgeTag(tag) }}
+                  </span>
+                </div> -->
+                <div class="option_list" v-if="sub.options.length">
+                  <div class="option_item" v-for="opt in sub.options" :key="opt.key">
+                    <span class="opt_key">{{ opt.key }}.</span>
+                    <span class="opt_text" v-html="opt.text"></span>
+                  </div>
                 </div>
               </div>
+
+              <div
+                v-if="!selectedQuestion.title && !selectedQuestion.options.length
+                  && !selectedQuestion.subQuestions.length"
+                class="content_empty no_content_data">
+                <span>暂无题干数据</span>
+              </div>
             </div>
-            <div class="detail_right">
-              <div class="detail_tabs">
-                <span
-                  class="detail_tab"
-                  :class="{ active: detailTab === 'answer' }"
-                  @click="detailTab = 'answer'">
-                  我的作答
-                </span>
-                <span
-                  class="detail_tab"
-                  :class="{ active: detailTab === 'analysis' }"
-                  @click="detailTab = 'analysis'">
-                  答案解析
+          </div>
+
+          <!-- 右侧:tabs 独立卡片 + 作答内容卡片,上下间隔 10px -->
+          <div class="answer_col">
+            <div class="card detail_tabs_card">
+              <el-radio-group v-model="detailTab" class="detail_radio">
+                <el-radio-button label="answer">我的作答</el-radio-button>
+                <el-radio-button label="analysis">答案解析</el-radio-button>
+              </el-radio-group>
+            </div>
+            <div class="card detail_card answer_card">
+              <div class="score_tags">
+                <span class="score_tag">得分:{{ selectedQuestion.score }}</span>
+                <span class="score_tag">得分率:{{ selectedQuestion.rate }}%</span>
+                <span v-if="selectedQuestion.modelText" class="model_tag" :class="`model_${selectedQuestion.model}`">
+                  {{ selectedQuestion.modelText }}
                 </span>
               </div>
-              <div class="score_row">
-                <span>得分:{{ selectedQuestion.score }}</span>
-                <span>得分率:{{ selectedQuestion.rate }}%</span>
+
+              <!-- 我的作答:writeLine 留痕图片 -->
+              <div v-if="detailTab === 'answer'" class="detail_content answer_content">
+                <template v-if="selectedQuestion.answerImages.length">
+                  <el-image
+                    v-for="(url, idx) in selectedQuestion.answerImages"
+                    :key="idx"
+                    class="answer_img"
+                    :src="url"
+                    fit="contain"
+                    :preview-src-list="selectedQuestion.answerImages"
+                    :z-index="3000">
+                  </el-image>
+                </template>
+                <div v-else class="content_empty no_content_data">
+                  <span>暂无作答内容</span>
+                </div>
               </div>
-              <div v-if="detailTab === 'answer'" class="answer_panel">
-                <div class="paper_box">
-                  <div class="paper_title">学生作答</div>
-                  <div class="paper_line" v-for="n in 6" :key="n"></div>
-                  <div class="paper_choice" v-if="selectedQuestion.userChoice">
-                    我的选择:{{ selectedQuestion.userChoice }}
+
+              <!-- 答案解析 -->
+              <div v-else class="detail_content analysis_content">
+                <template v-if="selectedQuestionHasAnalysis">
+                  <div v-if="hasHtmlContent(selectedQuestion.answer)" class="answer_line">
+                    <span class="line_label">【答案】</span>
+                    <span class="line_value answer_value" v-html="selectedQuestion.answer"></span>
                   </div>
+                  <div v-if="hasHtmlContent(selectedQuestion.parse)" class="parse_block" v-html="selectedQuestion.parse"></div>
+
+                  <div
+                    class="sub_analysis"
+                    v-for="(sub, sIdx) in selectedQuestion.subQuestions"
+                    :key="sub.id || sIdx">
+                    <div class="sub_analysis_title" v-if="sub.title" v-html="sub.title"></div>
+                    <div v-if="hasHtmlContent(sub.answer)" class="answer_line">
+                      <span class="line_label">【答案】</span>
+                      <span class="line_value answer_value" v-html="sub.answer"></span>
+                    </div>
+                    <div v-if="hasHtmlContent(sub.parse)" class="parse_block" v-html="sub.parse"></div>
+                  </div>
+                </template>
+                <div v-else class="content_empty no_content_data">
+                  <span>暂无解析数据</span>
                 </div>
               </div>
-              <div v-else class="analysis_panel">{{ selectedQuestion.analysis }}</div>
             </div>
           </div>
-        </section>
+        </div>
 
         <!-- 小题分析表 -->
         <section class="card" ref="qTable">
           <div class="card_head">
             <div class="card_title">小题分析表</div>
-            <el-button class="export_btn" @click="exportQuestionTable">导出Excel</el-button>
+            <div class="el_button">
+              <el-button class="export_btn" @click="exportQuestionTable">导出Excel</el-button>
+            </div>
           </div>
-          <el-table :data="questionPage" class="analysis_table">
-            <el-table-column prop="name" label="小题名称" min-width="120" />
-            <el-table-column prop="typeName" label="题型" min-width="100" />
-            <el-table-column prop="knowledge" label="知识点" min-width="180" />
-            <el-table-column prop="score" label="得分" min-width="80" />
-            <el-table-column label="得分率" min-width="100">
-              <template slot-scope="scope">{{ scope.row.rate }}%</template>
-            </el-table-column>
-          </el-table>
-          <div class="pager_row" v-if="questionRows.length">
-            <el-pagination
-              background
-              layout="prev, pager, next"
-              :current-page.sync="questionPageNum"
-              :page-size="pageSize"
-              :total="questionRows.length" />
+          <div class="page_table analysis_table_wrap">
+            <el-table
+              :data="questionPage"
+              height="572"
+              stripe
+              class="el-table--border analysis_table">
+              <el-table-column prop="name" label="小题名称" min-width="120" show-overflow-tooltip />
+              <el-table-column prop="typeName" label="题型" min-width="90" />
+              <el-table-column label="知识点" min-width="180" show-overflow-tooltip>
+                <template slot-scope="scope">{{ scope.row.knowledgeTags.join(',') }}</template>
+              </el-table-column>
+              <el-table-column label="得分" min-width="100">
+                <template slot-scope="scope">{{ scope.row.score }} / {{ scope.row.scoreFull }}</template>
+              </el-table-column>
+              <el-table-column label="得分率" min-width="100">
+                <template slot-scope="scope">{{ scope.row.rate }}%</template>
+              </el-table-column>
+            </el-table>
+            <div class="page_pagination total" v-if="questionRows.length">
+              <span class="page_total">共{{ questionRows.length }}条数据</span>
+              <el-pagination
+                background
+                layout="prev, pager, next"
+                :current-page.sync="questionPageNum"
+                :page-size="questionPageSize"
+                :total="Number(questionRows.length)" />
+            </div>
           </div>
         </section>
 
@@ -166,47 +256,63 @@
               </span>
             </div>
           </div>
-          <BarChart
-            v-if="kChartType === 'vertical_bar'"
-            :datax="knowledgeDatax"
-            :datay="knowledgeDatay"
-            typeName="得分率"
-            unit="%"
-            :gridLeft="0"
-            :gridRight="0" />
-          <RadarCharts
-            v-else
-            :data="knowledgeRadarData"
-            :showCheckBox="false"
-            :showLegend="false" />
+          <template v-if="knowledgeRows.length">
+            <BarChart
+              v-if="kChartType === 'vertical_bar'"
+              :datax="knowledgeDatax"
+              :datay="knowledgeDatay"
+              typeName="得分率"
+              unit="%"
+              :gridLeft="0"
+              :gridRight="0" />
+            <RadarCharts
+              v-else
+              :data="knowledgeRadarData"
+              :showCheckBox="false"
+              :showLegend="false" />
+          </template>
+          <div v-else-if="!loading" class="empty_tip no_content_data">
+            <span>暂无数据</span>
+          </div>
         </section>
 
         <!-- 知识点分析表 -->
         <section class="card" ref="kTable">
           <div class="card_head">
             <div class="card_title">知识点分析表</div>
-            <el-button class="export_btn" @click="exportKnowledgeTable">导出Excel</el-button>
+            <div class="el_button">
+              <el-button class="export_btn" @click="exportKnowledgeTable">导出Excel</el-button>
+            </div>
           </div>
-          <el-table :data="knowledgePage" class="analysis_table">
-            <el-table-column prop="name" label="知识点" min-width="200" />
-            <el-table-column prop="questions" label="包含小题" min-width="180" />
-            <el-table-column prop="score" label="得分" min-width="80" />
-            <el-table-column label="得分率" min-width="100">
-              <template slot-scope="scope">{{ scope.row.rate }}%</template>
-            </el-table-column>
-          </el-table>
-          <div class="pager_row" v-if="knowledgeRows.length">
-            <el-pagination
-              background
-              layout="prev, pager, next"
-              :current-page.sync="knowledgePageNum"
-              :page-size="pageSize"
-              :total="knowledgeRows.length" />
+          <div class="page_table analysis_table_wrap">
+            <el-table
+              :data="knowledgePage"
+              height="572"
+              stripe
+              class="el-table--border analysis_table">
+              <el-table-column prop="name" label="知识点" min-width="200" show-overflow-tooltip />
+              <el-table-column prop="questions" label="包含小题" min-width="180" show-overflow-tooltip />
+              <el-table-column label="得分" min-width="100">
+                <template slot-scope="scope">{{ scope.row.score }} / {{ scope.row.fullScore }}</template>
+              </el-table-column>
+              <el-table-column label="得分率" min-width="100">
+                <template slot-scope="scope">{{ scope.row.rate }}%</template>
+              </el-table-column>
+            </el-table>
+            <div class="page_pagination total" v-if="knowledgeRows.length">
+              <span class="page_total">共{{ knowledgeRows.length }}条数据</span>
+              <el-pagination
+                background
+                layout="prev, pager, next"
+                :current-page.sync="knowledgePageNum"
+                :page-size="knowledgePageSize"
+                :total="Number(knowledgeRows.length)" />
+            </div>
           </div>
         </section>
 
-        <!-- 学情总结 -->
-        <section class="card" ref="summary">
+        <!-- 学情总结(接口暂无该数据时隐藏) -->
+        <section class="card" ref="summary" v-if="hasSummary">
           <div class="card_title">学情总结</div>
           <div class="summary_block">
             <span class="summary_label strength">优势</span>
@@ -234,55 +340,122 @@ import RadarCharts from '@/views/analysisReport/components/dCharts/radarCharts';
 
 const STORAGE_KEY = 'teachingHomeworkAnalysis';
 
-const OPTIONS = [
-  { key: 'A', text: '考古发现可以直接替代文献记载' },
-  { key: 'B', text: '出土文物能够为历史研究提供实物证据' },
-  { key: 'C', text: '考古结论一经形成便不再修正' },
-  { key: 'D', text: '文献记载与考古发现必然完全一致' },
-];
-
-const EXTRA_TAGS = ['分析体裁特征和表现手法', '阅读理解'];
-
-function buildQuestionRows() {
-  const knowledgePool = ['作家作品', '分析体裁特征和表现手法', '阅读理解', '文言文阅读', '古诗词鉴赏', '语言表达'];
-  const rates = [96, 29, 88, 72, 65, 91, 54, 80, 43, 77];
-  const rows = [];
-  for (let i = 1; i <= 50; i += 1) {
-    const rate = rates[(i - 1) % rates.length];
-    const isChoice = i % 5 !== 0;
-    rows.push({
-      id: i,
-      name: `第${i}题`,
-      typeName: isChoice ? '选择题' : '解答题',
-      knowledge: knowledgePool[i % knowledgePool.length],
-      extraTags: EXTRA_TAGS,
-      score: ((rate / 100) * 5).toFixed(1),
-      rate,
-      stem: `下列关于考古发现与历史研究的表述,正确的一项是( )`,
-      options: isChoice ? OPTIONS : [],
-      userChoice: isChoice ? 'A' : '',
-      analysis: '本题考查对考古与历史研究关系的理解。出土文物可以为历史研究提供实物证据,正确答案为 B。',
+// 接口数值字段可能返回字符串,统一转数字
+function toNumber(val, fallback = 0) {
+  const n = Number(val);
+  return Number.isFinite(n) ? n : fallback;
+}
+
+const OPTION_KEYS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+// 题型:1-单选题 2-多选题 3-对错题(判断题)
+const QUESTION_TYPE_MAP = { 1: '单选题', 2: '多选题', 3: '判断题' };
+
+const ANSWER_MODEL_MAP = { 0: '', 1: '优秀作答', 2: '典型错误' };
+
+// 选项列表统一为 [{ key, text }],columnName 为 A/B/C 字段值,按 sort 排序
+function mapOptionList(list) {
+  if (!Array.isArray(list)) return [];
+  return [...list]
+    .sort((a, b) => toNumber(a.sort, 0) - toNumber(b.sort, 0))
+    .map((opt, idx) => {
+      if (opt == null) return { key: OPTION_KEYS[idx] || String(idx + 1), text: '' };
+      if (typeof opt === 'string') return { key: OPTION_KEYS[idx] || String(idx + 1), text: opt };
+      return {
+        key: opt.columnName || OPTION_KEYS[idx] || String(idx + 1),
+        text: opt.content || '',
+      };
     });
+}
+
+// 知识点列表统一为字符串数组
+function mapKnowledgeList(list, fallback) {
+  if (Array.isArray(list) && list.length) {
+    const names = list.map(item => (item && item.knowName) || '').filter(Boolean);
+    if (names.length) return names;
   }
-  return rows;
+  return fallback ? [String(fallback)] : [];
 }
 
-function buildKnowledgeRows() {
-  const names = ['作家作品', '分析体裁特征和表现手法', '阅读理解', '文言文实词', '古诗词鉴赏', '病句辨析', '语言表达', '名句默写', '现代文阅读', '作文立意'];
-  const rows = [];
-  for (let i = 0; i < 40; i += 1) {
-    const rate = 40 + ((i * 13) % 55);
-    const base = names[i % names.length];
-    const name = i >= names.length ? `${base}(${Math.floor(i / names.length) + 1})` : base;
-    rows.push({
-      id: i + 1,
-      name,
-      questions: `第${(i % 10) + 1}题、第${(i % 10) + 2}题`,
-      score: (rate / 20).toFixed(1),
-      rate,
-    });
+// writeLine 为作答留痕数据(JSON),兼容字符串/数组/对象多种形态,提取作答图片地址
+function parseWriteLine(raw) {
+  if (!raw) return [];
+  let data = raw;
+  if (typeof raw === 'string') {
+    const text = raw.trim();
+    if (!text) return [];
+    try {
+      data = JSON.parse(text);
+    } catch (e) {
+      // 非 JSON 的纯字符串视为单个图片地址
+      return /^https?:\/\//.test(text) ? [text] : [];
+    }
+  }
+  const pick = obj => {
+    if (!obj || typeof obj !== 'object') return [];
+    const urls = obj.writeUrls || obj.urls || obj.imgUrls || obj.images || obj.imgs;
+    if (Array.isArray(urls)) return urls;
+    return obj.url || obj.imageUrl ? [obj.url || obj.imageUrl] : [];
+  };
+  if (Array.isArray(data)) {
+    return data.reduce((acc, item) => {
+      if (typeof item === 'string') {
+        if (/^https?:\/\//.test(item)) acc.push(item);
+      } else {
+        pick(item).forEach(url => acc.push(url));
+      }
+      return acc;
+    }, []).filter(Boolean);
   }
-  return rows;
+  return pick(data).filter(Boolean);
+}
+
+// 子题(subQuestions)映射
+function mapSubQuestion(item, index) {
+  return {
+    id: item.questionId || index,
+    title: item.title || '',
+    answer: item.answer || '',
+    parse: item.parse || '',
+    options: mapOptionList(item.optionList),
+    typeName: item.questionTypeName || QUESTION_TYPE_MAP[item.questionTypeId] || '',
+    isJudge: item.isJudge,
+    knowledgeTags: mapKnowledgeList(item.knowledgeList),
+  };
+}
+
+// 学生题目列表(studentQuestionVOList)映射为小题行
+function mapQuestionRow(item, index) {
+  return {
+    id: item.questionId || item.id || index,
+    name: item.questionName || '-',
+    type: toNumber(item.questionType),
+    typeName: QUESTION_TYPE_MAP[item.questionType] || '',
+    knowledgeTags: mapKnowledgeList(item.knowledgeList, item.knowledge),
+    title: item.title || '',
+    answer: item.answer || '',
+    parse: item.parse || '',
+    options: mapOptionList(item.optionList),
+    answerImages: parseWriteLine(item.writeLine),
+    modelText: ANSWER_MODEL_MAP[item.model] || '',
+    model: toNumber(item.model),
+    score: item.score === '' || item.score == null ? '-' : item.score,
+    scoreFull: item.scoreFull === '' || item.scoreFull == null ? '-' : item.scoreFull,
+    rate: toNumber(item.scoreRate),
+    subQuestions: Array.isArray(item.subQuestions) ? item.subQuestions.map(mapSubQuestion) : [],
+  };
+}
+
+// 学生知识点列表(studentKnowVOList)映射为知识点行,scoreRate 已是百分数值只需加 %
+function mapKnowledgeRow(item, index) {
+  return {
+    id: index,
+    name: item.knowledge || item.questionName || '-',
+    questions: item.questionName || '',
+    score: item.score == null ? '-' : item.score,
+    fullScore: item.scoreFull == null ? '-' : item.scoreFull,
+    rate: toNumber(item.scoreRate),
+  };
 }
 
 export default {
@@ -294,10 +467,13 @@ export default {
   data() {
     return {
       homework: null,
+      loading: false,
       activeNav: 'overview',
       scrollLock: false,
       scrollTimer: null,
-      navList: [
+      // 导出中,防止重复点击
+      exporting: false,
+      allNavList: [
         { key: 'overview', label: '作业概况' },
         { key: 'qChart', label: '小题分析图' },
         { key: 'qTable', label: '小题分析表' },
@@ -306,34 +482,29 @@ export default {
         { key: 'summary', label: '学情总结' },
       ],
       overview: {
-        fullScore: 100,
-        score: 65,
-        rate: '63.26%',
-        correctCount: 9,
-        questionCount: 65,
+        fullScore: 0,
+        score: 0,
+        rate: 0,
+        correctCount: 0,
+        questionCount: 0,
       },
-      questionRows: buildQuestionRows(),
-      knowledgeRows: buildKnowledgeRows(),
+      questionRows: [],
+      knowledgeRows: [],
       qChartType: 'vertical_bar',
       kChartType: 'vertical_bar',
       chartTypeList: [
         { label: '柱状图', value: 'vertical_bar' },
         { label: '雷达图', value: 'radar_chart' },
       ],
-      selectedIndex: 1,
+      selectedIndex: 0,
       detailTab: 'answer',
       questionPageNum: 1,
       knowledgePageNum: 1,
-      pageSize: 5,
-      summary: {
-        strength: '本次作业总体得分稳定,基础题掌握较好,阅读理解类题目得分率较高。',
-        weakness: '第2题及部分文言、表达类小题得分偏低,知识点迁移还不够熟练。',
-        advice: [
-          '针对失分小题回看题干和解析,整理同类题的答题步骤。',
-          '优先复习得分率较低的知识点,并完成对应小题的订正。',
-          '解答题注意分点作答,先写结论再补充依据。',
-        ],
-      },
+      // 两个表格前端分页,固定每页 10 条
+      questionPageSize: 10,
+      knowledgePageSize: 10,
+      // 接口未返回学情总结,无数据时隐藏该区块
+      summary: null,
     };
   },
   computed: {
@@ -348,21 +519,36 @@ export default {
       const name = (this.homework && this.homework.subjectName) || '';
       return this.$global.getSubjectsName(name, null, 'color');
     },
+    hasSummary() {
+      return !!(this.summary && (this.summary.strength || this.summary.weakness
+        || (Array.isArray(this.summary.advice) && this.summary.advice.length)));
+    },
+    navList() {
+      return this.allNavList.filter(item => item.key !== 'summary' || this.hasSummary);
+    },
     questionCount() {
-      if (this.homework && this.homework.questionNum != null) return this.homework.questionNum;
-      return this.overview.questionCount;
+      if (this.overview.questionCount) return this.overview.questionCount;
+      return this.homework && this.homework.questionNum != null ? this.homework.questionNum : 0;
     },
     overviewList() {
       return [
         { key: 'full', label: '满分', value: this.overview.fullScore, icon: require('@/assets/homeworkTask/fullMarks.png') },
         { key: 'score', label: '得分', value: this.overview.score, icon: require('@/assets/homeworkTask/score.png') },
-        { key: 'rate', label: '得分率', value: this.overview.rate, icon: require('@/assets/homeworkTask/scoringRate.png') },
+        { key: 'rate', label: '得分率', value: `${this.overview.rate}%`, icon: require('@/assets/homeworkTask/scoringRate.png') },
         { key: 'correct', label: '答对题数', value: this.overview.correctCount, icon: require('@/assets/homeworkTask/correct.png') },
-        { key: 'count', label: '试题数量', value: this.overview.questionCount, icon: require('@/assets/homeworkTask/quantity.png') },
+        { key: 'count', label: '试题数量', value: this.questionCount, icon: require('@/assets/homeworkTask/quantity.png') },
       ];
     },
+    // 小题分析图展示全部小题
     chartQuestions() {
-      return this.questionRows.slice(0, 10);
+      return this.questionRows;
+    },
+    // 答案解析 tab 是否有内容(答案/解析,或子题的答案/解析)
+    selectedQuestionHasAnalysis() {
+      const q = this.selectedQuestion;
+      if (!q) return false;
+      if (this.hasHtmlContent(q.answer) || this.hasHtmlContent(q.parse)) return true;
+      return q.subQuestions.some(sub => this.hasHtmlContent(sub.answer) || this.hasHtmlContent(sub.parse));
     },
     chartKnowledge() {
       return this.knowledgeRows.slice(0, 8);
@@ -371,12 +557,12 @@ export default {
       return this.chartQuestions[this.selectedIndex] || null;
     },
     questionPage() {
-      const start = (this.questionPageNum - 1) * this.pageSize;
-      return this.questionRows.slice(start, start + this.pageSize);
+      const start = (this.questionPageNum - 1) * this.questionPageSize;
+      return this.questionRows.slice(start, start + this.questionPageSize);
     },
     knowledgePage() {
-      const start = (this.knowledgePageNum - 1) * this.pageSize;
-      return this.knowledgeRows.slice(start, start + this.pageSize);
+      const start = (this.knowledgePageNum - 1) * this.knowledgePageSize;
+      return this.knowledgeRows.slice(start, start + this.knowledgePageSize);
     },
     questionDatax() {
       return this.chartQuestions.map(item => item.name);
@@ -416,6 +602,66 @@ export default {
         return;
       }
       this.homework = cache.homework;
+      this.fetchAnalysis();
+    },
+    fetchAnalysis() {
+      const homework = this.homework || {};
+      const aidPaperClassId = homework.aidPaperClassId || homework.homeWorkId;
+      const userInfo = (this.$store.state.user && this.$store.state.user.userInfo) || {};
+      // 学生账号即学籍号
+      const registrationCode = userInfo.registrationCode || userInfo.loginName || '';
+      if (!aidPaperClassId || !registrationCode) {
+        this.resetAnalysis();
+        return;
+      }
+      this.loading = true;
+      const params = { aidPaperClassId, registrationCode };
+      this.$api.teachingHomework.getStudentAnalysis(params).then(res => {
+        this.applyAnalysis(res);
+      }).catch(err => {
+        const body = this.pickBody(err);
+        if (body) {
+          this.applyAnalysis(body);
+          return;
+        }
+        this.resetAnalysis();
+      }).finally(() => {
+        this.loading = false;
+      });
+    },
+    // 公共请求把 code 为 0 的成功响应放进了 reject,这里两种都认
+    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;
+    },
+    resetAnalysis() {
+      this.overview = { fullScore: 0, score: 0, rate: 0, correctCount: 0, questionCount: 0 };
+      this.questionRows = [];
+      this.knowledgeRows = [];
+      this.selectedIndex = 0;
+      this.questionPageNum = 1;
+      this.knowledgePageNum = 1;
+    },
+    applyAnalysis(res) {
+      const data = (res && res.data) || {};
+      this.overview = {
+        fullScore: toNumber(data.fullScore),
+        score: toNumber(data.studentScore),
+        rate: toNumber(data.rateScore),
+        correctCount: toNumber(data.rightNum),
+        questionCount: toNumber(data.questionNum),
+      };
+      // 新结构:studentQuestionVOList 直接为题目数组,不再分组
+      const questionList = Array.isArray(data.studentQuestionVOList) ? data.studentQuestionVOList : [];
+      this.questionRows = questionList.map(mapQuestionRow);
+      const knowList = Array.isArray(data.studentKnowVOList) ? data.studentKnowVOList : [];
+      this.knowledgeRows = knowList.map(mapKnowledgeRow);
+      this.selectedIndex = 0;
+      this.questionPageNum = 1;
+      this.knowledgePageNum = 1;
     },
     scrollTo(key) {
       const el = this.$refs[key];
@@ -423,7 +669,12 @@ export default {
       if (!el || !box) return;
       this.activeNav = key;
       this.scrollLock = true;
-      const top = el.getBoundingClientRect().top - box.getBoundingClientRect().top + box.scrollTop;
+      // 目标位置不能超过容器最大滚动距离(最后一个区块过高时顶部无法对齐容器顶部)
+      const max = box.scrollHeight - box.clientHeight;
+      const top = Math.min(
+        el.getBoundingClientRect().top - box.getBoundingClientRect().top + box.scrollTop,
+        max
+      );
       box.scrollTo({ top, behavior: 'smooth' });
       clearTimeout(this.scrollTimer);
       const start = Date.now();
@@ -441,12 +692,18 @@ export default {
     onScroll() {
       if (this.scrollLock) return;
       const box = this.$refs.mainScroll;
-      if (!box) return;
-      const scrollTop = box.scrollTop + 12;
+      if (!box || !this.navList.length) return;
+      // 已滚动到底部时,高亮最后一个导航项(末位区块过高、顶部无法到达容器顶部的场景)
+      if (box.scrollTop + box.clientHeight >= box.scrollHeight - 2) {
+        this.activeNav = this.navList[this.navList.length - 1].key;
+        return;
+      }
+      const boxTop = box.getBoundingClientRect().top;
       let current = this.navList[0].key;
       this.navList.forEach(item => {
         const el = this.$refs[item.key];
-        if (el && el.offsetTop <= scrollTop) current = item.key;
+        // 用相对滚动容器的视口距离判定,避免 offsetTop 受 offsetParent 影响
+        if (el && el.getBoundingClientRect().top - boxTop <= 12) current = item.key;
       });
       this.activeNav = current;
     },
@@ -455,43 +712,90 @@ export default {
       this.selectedIndex = index;
       this.detailTab = 'answer';
     },
-    downloadExcel(filename, columns, rows) {
-      const head = columns.map(col => `<th>${col.label}</th>`).join('');
-      const body = rows.map(row => {
-        const tds = columns.map(col => `<td>${row[col.prop] == null ? '' : row[col.prop]}</td>`).join('');
-        return `<tr>${tds}</tr>`;
-      }).join('');
-      const html = `<html><head><meta charset="utf-8"></head><body><table border="1"><tr>${head}</tr>${body}</table></body></html>`;
-      const blob = new Blob(['\ufeff', html], { type: 'application/vnd.ms-excel' });
-      const url = URL.createObjectURL(blob);
-      const link = document.createElement('a');
-      link.href = url;
-      link.download = filename;
-      link.click();
-      URL.revokeObjectURL(url);
+    // 判断富文本是否有实际内容:纯 <p><br></p> 等空标签视为空,含图片或文字视为有内容
+    hasHtmlContent(content) {
+      if (!content) return false;
+      if (/<img[\s>]/i.test(content)) return true;
+      const text = String(content).replace(/<[^>]*>/g, '').replace(/&nbsp;/g, '').trim();
+      return text.length > 0;
+    },
+    // 单元格空值统一显示 '-'
+    excelCell(value) {
+      return value === null || value === undefined || value === '' ? '-' : value;
+    },
+    // 调用通用导出接口并触发浏览器下载
+    downloadAnalysisExcel(payload) {
+      if (this.exporting) return;
+      this.exporting = true;
+      this.$api.teachingHomework.exportStudentAnalysis(payload).then(blob => {
+        // 后端业务异常时可能返回 JSON(仍被包装成 blob),读取后提示
+        if (blob && blob.type && blob.type.indexOf('application/json') !== -1) {
+          const reader = new FileReader();
+          reader.onload = () => {
+            try {
+              const msg = JSON.parse(reader.result).message || '导出失败';
+              this.$message.error(msg);
+            } catch (e) {
+              this.$message.error('导出失败');
+            }
+          };
+          reader.readAsText(blob);
+          return;
+        }
+        const excelBlob = new Blob([blob], {
+          type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+        });
+        const link = document.createElement('a');
+        link.href = URL.createObjectURL(excelBlob);
+        link.download = payload.fileName;
+        link.click();
+        URL.revokeObjectURL(link.href);
+      }).catch(() => {
+        this.$message.error('导出失败,请稍后重试');
+      }).finally(() => {
+        this.exporting = false;
+      });
     },
     exportQuestionTable() {
-      this.downloadExcel('小题分析表.xls', [
-        { label: '小题名称', prop: 'name' },
-        { label: '题型', prop: 'typeName' },
-        { label: '知识点', prop: 'knowledge' },
-        { label: '得分', prop: 'score' },
-        { label: '得分率', prop: 'rateText' },
-      ], this.questionRows.map(item => ({ ...item, rateText: `${item.rate}%` })));
+      const payload = {
+        fileName: '小题分析表.xlsx',
+        sheetName: '小题分析表',
+        headDataList: [['小题名称', '题型', '知识点', '得分', '得分率']],
+        dataList: this.questionRows.map(item => [
+          this.excelCell(item.name),
+          this.excelCell(item.typeName),
+          this.excelCell(item.knowledgeTags.join(',')),
+          `${this.excelCell(item.score)} / ${this.excelCell(item.scoreFull)}`,
+          `${this.excelCell(item.rate)}%`,
+        ]),
+      };
+      this.downloadAnalysisExcel(payload);
     },
     exportKnowledgeTable() {
-      this.downloadExcel('知识点分析表.xls', [
-        { label: '知识点', prop: 'name' },
-        { label: '包含小题', prop: 'questions' },
-        { label: '得分', prop: 'score' },
-        { label: '得分率', prop: 'rateText' },
-      ], this.knowledgeRows.map(item => ({ ...item, rateText: `${item.rate}%` })));
+      const payload = {
+        fileName: '知识点分析表.xlsx',
+        sheetName: '知识点分析表',
+        headDataList: [['知识点', '包含小题', '得分', '得分率']],
+        dataList: this.knowledgeRows.map(item => [
+          this.excelCell(item.name),
+          this.excelCell(item.questions),
+          `${this.excelCell(item.score)} / ${this.excelCell(item.fullScore)}`,
+          `${this.excelCell(item.rate)}%`,
+        ]),
+      };
+      this.downloadAnalysisExcel(payload);
     },
     handleAiExplain() {
       this.$message.info('AI讲解待对接');
     },
+    // 知识点标签超过 5 个字截断并补省略号,完整内容通过 title 悬浮展示
+    formatKnowledgeTag(tag) {
+      if (!tag) return '';
+      return tag.length > 5 ? `${tag.slice(0, 5)}…` : tag;
+    },
     goBack() {
-      this.$router.push('/teachingHomework/list');
+      // 从查看分析返回时,列表定位到「已提交」页签
+      this.$router.push({ path: '/teachingHomework/list', query: { status: 1 } });
     },
   },
 };
@@ -631,6 +935,13 @@ export default {
       justify-content: space-between;
       margin-bottom: 12px;
     }
+
+    .empty_tip {
+      min-height: 200px;
+      height: 200px;
+      width: 100%;
+      border-radius: 8px;
+    }
   }
 
   .metric_row {
@@ -644,7 +955,7 @@ export default {
       display: flex;
       align-items: center;
       gap: 10px;
-      padding: 14px 16px;
+      padding: 16px 13px;
       background: #fff;
       border: 1px solid #EBEEF5;
       border-radius: 8px;
@@ -676,190 +987,376 @@ export default {
     }
   }
 
-  .question_detail {
+  /* 选中小题:题干 / 我的作答 两张独立卡片 */
+  .question_detail_wrap {
     display: flex;
-    gap: 16px;
-    margin-top: 8px;
-    padding-top: 16px;
-    border-top: 1px solid #EBEEF5;
+    align-items: stretch;
+    gap: 10px;
+    margin-bottom: 10px;
+
+    .detail_card {
+      min-width: 0;
+      margin-bottom: 0;
+      display: flex;
+      flex-direction: column;
+    }
+
+    /* 左题干卡固定宽度 */
+    .stem_card {
+      flex: 0 0 410px;
+      width: 410px;
+      padding: 20px;
+    }
 
-    .detail_left,
-    .detail_right {
+    /* 右列:tabs 卡 + 作答卡 上下排列,间隔 10px */
+    .answer_col {
       flex: 1;
       min-width: 0;
+      display: flex;
+      flex-direction: column;
+      gap: 10px;
+
+      /* 下方作答卡占满剩余高度 */
+      .answer_card {
+        flex: 1;
+        min-width: 0;
+        min-height: 0;
+      }
+    }
+
+    /* tabs 独立卡片,切换按钮复用全局 el-radio-button 公共用法(同 list.vue) */
+    .detail_tabs_card {
+      flex-shrink: 0;
+      margin-bottom: 0;
+      padding: 10px 20px;
+
+      .detail_radio {
+        :deep(.el-radio-button__inner) {
+          width: 76px;
+          height: 36px;
+          line-height: 36px;
+          padding: 0;
+          font-size: 14px;
+          color: #606266;
+          background: #fff;
+          border-color: #DCDFE6;
+          box-shadow: none !important;
+        }
+
+        :deep(.el-radio-button:first-child .el-radio-button__inner) {
+          border-radius: 4px 0 0 4px;
+        }
+
+        :deep(.el-radio-button:last-child .el-radio-button__inner) {
+          border-radius: 0 4px 4px 0;
+        }
+
+        :deep(.el-radio-button__orig-radio:checked + .el-radio-button__inner) {
+          color: #fff;
+          background-color: #2E64FA;
+          border-color: #2E64FA;
+          box-shadow: none !important;
+        }
+      }
     }
 
     .detail_head {
+      flex-shrink: 0;
       display: flex;
       align-items: center;
       justify-content: space-between;
-      margin-bottom: 12px;
+      margin-bottom: 16px;
     }
 
     .detail_title {
-      font-size: 15px;
+      font-size: 16px;
       font-weight: 550;
       color: #333;
+      line-height: 24px;
     }
 
     .ai_btn {
-      height: 32px;
-      padding: 0 12px;
+      height: 36px;
+      padding: 0 20px;
       border: none;
-      border-radius: 4px;
-      background: #847BE6;
+      border-radius: 6px;
+      background: linear-gradient(270deg, #6659FF 0%, #9A57FF 60.38%, #F292FF 100%);
       color: #fff;
+      font-size: 14px;
 
       &:hover,
       &:focus {
-        background: #847BE6;
-        color: #fff;
         opacity: 0.92;
+        color: #fff;
+        background: linear-gradient(270deg, #6659FF 0%, #9A57FF 60.38%, #F292FF 100%);
       }
     }
 
     .knowledge_tags {
+      flex-shrink: 0;
       display: flex;
       flex-wrap: wrap;
+      align-items: center;
       gap: 8px;
-      margin-bottom: 12px;
+      margin-bottom: 16px;
+
+      .knowledge_label {
+        font-size: 14px;
+        font-weight: 550;
+        color: #333;
+      }
 
       .knowledge_tag {
         display: inline-flex;
         align-items: center;
-        height: 28px;
-        padding: 0 10px;
-        border: 1px solid #409EFF;
+        height: 30px;
+        padding: 0 12px;
+        border: 1px solid #2E64FA;
         border-radius: 4px;
-        color: #409EFF;
+        color: #2E64FA;
         font-size: 13px;
+        line-height: 1;
         background: #fff;
         box-sizing: border-box;
+        white-space: nowrap;
+
+        &.sm {
+          height: 26px;
+          font-size: 12px;
+        }
+      }
+    }
+
+    .stem_scroll {
+      flex: 1;
+      min-height: 0;
+      overflow-y: auto;
+      padding-right: 6px;
+    }
+
+    .stem_text {
+      font-weight: 550;
+      font-size: 16px;
+      line-height: 1.8;
+      color: #333333;
+
+      :deep(img) {
+        max-width: 100%;
+        vertical-align: middle;
       }
     }
 
-    .stem_text,
     .option_item {
+      display: flex;
+      align-items: flex-start;
+      margin-top: 12px;
       font-size: 14px;
       line-height: 1.7;
       color: #333;
+
+      .opt_key {
+        flex-shrink: 0;
+        margin-right: 6px;
+      }
+
+      .opt_text {
+        flex: 1;
+        min-width: 0;
+
+        :deep(img) {
+          max-width: 100%;
+          vertical-align: middle;
+        }
+      }
     }
 
-    .option_item {
-      margin-top: 6px;
+    .sub_question {
+      padding-top: 16px;
+
+      .sub_title {
+        font-size: 14px;
+        line-height: 1.8;
+        color: #333;
+        font-weight: 550;
+
+        :deep(img) {
+          max-width: 100%;
+          vertical-align: middle;
+        }
+      }
+
+      .knowledge_tags {
+        margin: 10px 0 0;
+      }
+
+      .option_item:first-child {
+        margin-top: 12px;
+      }
     }
 
-    .detail_tabs {
+    /* tabs 已抽为上方独立卡片,使用公共 el-radio-button 样式 */
+
+    .score_tags {
+      flex-shrink: 0;
       display: flex;
-      align-items: center;
-      gap: 24px;
-      margin-bottom: 12px;
+      flex-wrap: wrap;
+      gap: 12px;
+      margin-bottom: 16px;
+
+      .score_tag {
+        display: inline-flex;
+        align-items: center;
+        height: 38px;
+        padding: 0 16px;
+        background: #EAF1FF;
+        border-radius: 6px;
+        color: #2E64FA;
+        font-size: 14px;
+        box-sizing: border-box;
+      }
 
-      .detail_tab {
-        position: relative;
-        height: 32px;
-        line-height: 32px;
+      .model_tag {
+        display: inline-flex;
+        align-items: center;
+        height: 38px;
+        padding: 0 16px;
+        border-radius: 6px;
         font-size: 14px;
-        color: #909399;
-        cursor: pointer;
+        box-sizing: border-box;
 
-        &.active {
-          color: #409EFF;
-          font-weight: 550;
+        &.model_1 {
+          background: #E8F7EE;
+          color: #67C23A;
+        }
 
-          &::after {
-            content: '';
-            position: absolute;
-            left: 0;
-            right: 0;
-            bottom: 0;
-            height: 2px;
-            background: #409EFF;
-          }
+        &.model_2 {
+          background: #FEF0F0;
+          color: #F56C6C;
         }
       }
     }
 
-    .score_row {
-      display: flex;
-      gap: 16px;
-      margin-bottom: 12px;
+    .detail_content {
+      flex: 1;
+      min-height: 360px;
+      overflow-y: auto;
+      padding: 16px;
+      background: #F5F7FA;
+      border-radius: 8px;
+      box-sizing: border-box;
       font-size: 14px;
-      color: #409EFF;
+      line-height: 1.7;
+      color: #666;
     }
 
-    .paper_box {
-      min-height: 220px;
-      padding: 16px;
-      border: 1px solid #EBEEF5;
-      border-radius: 4px;
-      background: #FAFBFC;
-      box-sizing: border-box;
+    .answer_content {
+      text-align: center;
+      display: flex;
 
-      .paper_title {
-        margin-bottom: 12px;
-        font-size: 14px;
-        color: #333;
+      .answer_img {
+        display: block;
+        width: 100%;
+        max-width: 560px;
+        margin: 0 auto;
+        border-radius: 4px;
+        background: #fff;
+
+        :deep(img) {
+          width: 100%;
+          height: auto;
+          display: block;
+        }
+
+        & + .answer_img {
+          margin-top: 12px;
+        }
       }
+    }
 
-      .paper_line {
-        height: 10px;
-        margin-bottom: 10px;
-        background: #EEF1F6;
-        border-radius: 2px;
+    .analysis_content {
+      .answer_line {
+        display: flex;
+        align-items: flex-start;
+        font-size: 14px;
+        line-height: 1.8;
 
-        &:nth-child(odd) {
-          width: 92%;
+        .line_label {
+          flex-shrink: 0;
+          color: #333;
+          font-weight: 550;
         }
 
-        &:nth-child(even) {
-          width: 70%;
+        .answer_value {
+          flex: 1;
+          min-width: 0;
+          color: #2E64FA;
+          font-weight: 550;
+
+          :deep(p) {
+            margin: 0;
+          }
+
+          :deep(img) {
+            max-width: 100%;
+            vertical-align: middle;
+          }
         }
       }
 
-      .paper_choice {
+      .parse_block {
         margin-top: 8px;
         font-size: 14px;
+        line-height: 1.8;
         color: #666;
+
+        :deep(p) {
+          margin: 0;
+        }
+
+        :deep(img) {
+          max-width: 100%;
+          vertical-align: middle;
+        }
+      }
+
+      .sub_analysis {
+        margin-top: 18px;
+        padding-top: 14px;
+        border-top: 1px dashed #DCDFE6;
+
+        .sub_analysis_title {
+          font-size: 14px;
+          font-weight: 550;
+          color: #333;
+          line-height: 1.8;
+          margin-bottom: 6px;
+
+          :deep(img) {
+            max-width: 100%;
+            vertical-align: middle;
+          }
+        }
       }
     }
 
-    .analysis_panel {
-      min-height: 220px;
-      padding: 16px;
-      border: 1px solid #EBEEF5;
-      border-radius: 4px;
-      background: #FAFBFC;
-      font-size: 14px;
-      line-height: 1.7;
-      color: #666;
-      box-sizing: border-box;
+    .content_empty {
+      flex: 1;
+      min-height: 200px;
     }
   }
 
-  .export_btn {
-    height: 32px;
-    padding: 0 12px;
-    color: #2E64FA;
-    border-color: #2E64FA;
-    background: #fff;
-  }
+  /* 导出按钮使用全局 .el_button .export_btn 公共样式(导出图标 + hover 变蓝) */
 
-  .analysis_table {
+  /* 表格使用全局 .page_table 公共样式,这里只控制宽度与分页间距 */
+  .analysis_table_wrap {
     width: 100%;
 
-    :deep(th) {
-      background: #F5F7FA;
-      color: #333;
-      font-weight: 550;
+    .el-table {
+      width: 100%;
     }
   }
 
-  .pager_row {
-    display: flex;
-    justify-content: flex-end;
-    margin-top: 12px;
-  }
+  /* 分页器使用全局 .page_pagination 公共样式 */
 
   .summary_block {
     display: flex;

+ 214 - 80
src/views/teachingHomework/homeworkTask/answer.vue

@@ -22,7 +22,7 @@
     </div>
 
     <!-- 主体:左侧试题序号,右侧题干 + 底部固定作答区 -->
-    <div class="answer_body">
+      <div class="answer_body" v-loading="loading">
       <!-- 试题列表:单选题 / 解答题分组,序号可切换当前题 -->
       <div class="question_nav">
         <div class="nav_title">试题列表</div>
@@ -45,8 +45,8 @@
             <span class="q_type">{{ currentQuestion.typeName }}</span>
             <span class="q_level">{{ currentQuestion.difficulty }}</span>
           </div>
-          <div class="q_stem">{{ currentQuestion.stem }}</div>
-          <div v-if="currentQuestion.type === 'single'" class="q_options">
+          <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>
@@ -79,7 +79,7 @@
           </div>
           <!-- 选择题禁用拍照;解答题可上传。最后一题把「下一题」换成「提交」 -->
           <div class="dock_actions">
-            <el-button class="photo_btn" :type="isSingle ? 'default' : 'primary'" :disabled="isSingle"
+            <el-button class="photo_btn" :type="isSingle ? 'default' : 'primary'" :disabled="isSingle" :loading="uploading"
               @click="handlePhoto">
               <img class="camera_icon" :src="isSingle ? cameraGray : cameraWhite" alt="">
               拍照上传
@@ -87,10 +87,10 @@
             <el-button class="nav_btn" :disabled="currentQuestion.index === 0" @click="prevQuestion">
               上一题
             </el-button>
-            <el-button v-if="!isLastQuestion" class="nav_btn" @click="nextQuestion">
+            <el-button v-if="!isLastQuestion" class="nav_btn" :loading="saving" @click="nextQuestion">
               下一题
             </el-button>
-            <el-button v-else class="nav_btn" @click="submitAnswer">
+            <el-button v-else class="nav_btn" :loading="saving" @click="submitAnswer">
               提交
             </el-button>
           </div>
@@ -115,40 +115,38 @@ import ElImageViewer from 'element-ui/packages/image/src/image-viewer';
 // 作答状态只存 sessionStorage,刷新仍停留本页,且地址栏不带参数
 const STORAGE_KEY = 'teachingHomeworkAnswer';
 
-/**
- * 临时试题数据,接口就绪后替换
- * type: single 选择题(答案为选项字符串) / photo 解答题(答案为图片数组)
- */
-function buildQuestions() {
-  const options = [
-    { key: 'A', text: '之:代词,指代前文所述内容' },
-    { key: 'B', text: '其:代词,他的' },
-    { key: 'C', text: '而:连词,表转折' },
-    { key: 'D', text: '以:介词,用、拿' },
-  ];
-  const list = [];
-  for (let i = 1; i <= 11; i += 1) {
-    list.push({
-      id: i,
-      type: 'single',
-      typeName: '选择题',
-      groupName: '单选题',
-      difficulty: '容易',
-      stem: `${i}、下列各句中,加点词的意义和用法相同的一组是`,
-      options,
-    });
-  }
-  for (let i = 12; i <= 16; i += 1) {
-    list.push({
-      id: i,
-      type: 'photo',
-      typeName: '解答题',
-      groupName: '解答题',
-      difficulty: '中等',
-      stem: `${i}、阅读材料,结合课文内容简要分析,并写出你的理解。`,
-    });
+const OPTION_KEYS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+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 list;
+  return options;
+}
+
+function mapQuestion(item, index) {
+  const choice = isChoiceQuestion(item);
+  const id = item.homeWorkStuQuesId || item.questionId || index;
+  return {
+    id,
+    questionId: item.questionId,
+    homeWorkStuQuesId: item.homeWorkStuQuesId,
+    type: choice ? 'single' : 'photo',
+    typeName: item.questionTypeName || (choice ? '客观题' : '主观题'),
+    groupName: item.questionTypeName || (choice ? '客观题' : '主观题'),
+    difficulty: item.difficultyName || '',
+    stem: item.questionContent || '',
+    options: choice ? buildOptions(item) : [],
+    writeAnswer: item.writeAnswer || '',
+    writeUrls: Array.isArray(item.writeUrls) ? item.writeUrls : [],
+    submitEndTime: item.submitEndTime || '',
+  };
 }
 
 export default {
@@ -159,15 +157,18 @@ export default {
   data() {
     return {
       homework: null, // 当前作业任务,来自 sessionStorage
-      questions: buildQuestions(), // 试题列表
+      questions: [],
+      loading: false,
       currentIndex: 0, // 当前题下标
       answers: {}, // 选择题为字符串,拍照题为图片 dataURL 数组
-      remainSeconds: 10 * 60, // 倒计时剩余秒数
-      timer: null,
       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: {
@@ -207,6 +208,10 @@ export default {
     isSingle() {
       return !!(this.currentQuestion && this.currentQuestion.type === 'single');
     },
+    hasOptionText() {
+      const options = (this.currentQuestion && this.currentQuestion.options) || [];
+      return options.some(opt => opt.text);
+    },
     // 是否最后一题,最后一题显示「提交」
     isLastQuestion() {
       return this.currentIndex >= this.questions.length - 1;
@@ -221,16 +226,28 @@ export default {
     answeredCount() {
       return this.questions.filter(item => this.isAnswered(item.id)).length;
     },
-    // 倒计时 mm:ss
+    // 倒计时:截止日期减去当前时间,按天、时、分、秒显示,并每秒刷新
     countdownText() {
-      const total = Math.max(0, this.remainSeconds);
-      const minute = String(Math.floor(total / 60)).padStart(2, '0');
-      const second = String(total % 60).padStart(2, '0');
-      return `${minute}:${second}`;
+      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();
@@ -251,40 +268,80 @@ export default {
       }
       this.homework = cache.homework;
       this.currentIndex = Number(cache.currentIndex) || 0;
-      const savedAnswers = cache.answers && typeof cache.answers === 'object' ? cache.answers : {};
+      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 list = Array.isArray(res && res.data) ? res.data : [];
+      this.questions = list.map(mapQuestion);
+      const endTime = (this.questions[0] && this.questions[0].submitEndTime) || 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();
+    },
+    fillAnswers() {
+      const savedAnswers = this.savedAnswers || {};
       const answers = {};
       this.questions.forEach(item => {
-        const saved = savedAnswers[item.id];
+        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] = Array.isArray(saved) ? saved : [];
+          answers[item.id] = hasSaved && Array.isArray(saved) ? saved : item.writeUrls.slice();
+        } else if (hasSaved && !Array.isArray(saved)) {
+          answers[item.id] = saved;
         } else {
-          answers[item.id] = saved != null && !Array.isArray(saved) ? saved : '';
+          answers[item.id] = item.writeAnswer || '';
         }
       });
       this.answers = answers;
-      this.remainSeconds = Number(cache.remainSeconds);
-      if (!Number.isFinite(this.remainSeconds)) this.remainSeconds = 10 * 60;
-      if (this.currentIndex < 0 || this.currentIndex >= this.questions.length) {
-        this.currentIndex = 0;
-      }
-      this.startTimer();
     },
-    // 保存作业、当前题、答案和剩余倒计时,保证刷新不丢失
+    // 保存作业、当前题和答案,保证刷新不丢失
     persistState() {
       if (!this.homework) return;
       sessionStorage.setItem(STORAGE_KEY, JSON.stringify({
         homework: this.homework,
         currentIndex: this.currentIndex,
         answers: this.answers,
-        remainSeconds: this.remainSeconds,
       }));
     },
-    // 倒计时每秒写入本地,刷新后从剩余时间继续
     startTimer() {
       this.clearTimer();
       this.timer = setInterval(() => {
-        if (this.remainSeconds > 0) this.remainSeconds -= 1;
-        this.persistState();
+        this.nowTick = Date.now();
       }, 1000);
     },
     clearTimer() {
@@ -318,32 +375,64 @@ export default {
       this.currentIndex -= 1;
       this.persistState();
     },
-    // 下一题;最后一题不走这里,按钮会换成提交
+    // 下一题先校验并保存,最后一题不走这里
     nextQuestion() {
       if (this.currentIndex >= this.questions.length - 1) return;
-      this.currentIndex += 1;
-      this.persistState();
+      this.saveCurrentAnswer(0).then(ok => {
+        if (!ok) return;
+        this.currentIndex += 1;
+        this.persistState();
+      });
     },
     // 选择题不可拍照;解答题打开本地选图
     handlePhoto() {
-      if (this.isSingle) return;
+      if (this.isSingle || this.uploading) return;
       const input = this.$refs.photoInput;
       if (input) input.click();
     },
-    // 选中图片后转成 dataURL,追加到当前题
+    // 上传图片后保存接口返回的文件地址
     onPhotoChange(e) {
       const file = e.target.files && e.target.files[0];
       e.target.value = '';
-      if (!file || !this.currentQuestion) return;
-      const reader = new FileReader();
-      reader.onload = () => {
-        const id = this.currentQuestion.id;
-        const list = Array.isArray(this.answers[id]) ? this.answers[id].slice() : [];
-        list.push(reader.result);
-        this.$set(this.answers, id, list);
-        this.persistState();
-      };
-      reader.readAsDataURL(file);
+      if (!file || !this.currentQuestion || this.isSingle) return;
+      const formData = new FormData();
+      formData.append('file', file);
+      this.uploading = true;
+      this.$api.teachingHomework.uploadFilesSelect(formData).then(res => {
+        this.appendPhotoUrl(this.pickFileUrl(res));
+      }).catch(err => {
+        const body = this.pickBody(err);
+        const url = body ? this.pickFileUrl(body) : '';
+        if (url) {
+          this.appendPhotoUrl(url);
+          return;
+        }
+        this.$message.warning('图片上传失败');
+      }).finally(() => {
+        this.uploading = false;
+      });
+    },
+    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) {
@@ -362,10 +451,51 @@ export default {
     closePhotoPreview() {
       this.imgVisible = false;
     },
-    // 最后一题提交,接口未就绪
+    // 当前题未作答时提示题号;已作答则保存。最后一题 commitFlag 为 1
+    saveCurrentAnswer(commitFlag) {
+      const question = this.currentQuestion;
+      if (!question || this.saving || this.uploading) {
+        if (this.uploading) this.$message.warning('图片正在上传,请稍候');
+        return Promise.resolve(false);
+      }
+      if (!this.isAnswered(question.id)) {
+        this.$message.warning(`第${question.index + 1}题没有上传答案,请作答`);
+        return Promise.resolve(false);
+      }
+      const isChoice = question.type === 'single';
+      const payload = {
+        homeWorkId: this.homework && this.homework.homeWorkId,
+        homeWorkStuId: this.homework && this.homework.homeWorkStuId,
+        homeWorkStuQuesId: question.homeWorkStuQuesId,
+        questionId: question.questionId,
+        commitFlag,
+        writeAnswer: isChoice ? (this.answers[question.id] || '') : '',
+        writeUrls: 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() {
-      this.persistState();
-      this.$message.success('提交待对接');
+      this.saveCurrentAnswer(1).then(ok => {
+        if (!ok) return;
+        this.persistState();
+        this.$message.success('提交成功');
+        this.$router.push('/teachingHomework/list');
+      });
     },
     // 返回列表,作答进度仍保留在 sessionStorage
     goBack() {
@@ -595,6 +725,10 @@ export default {
           line-height: 1.7;
           color: #333;
           margin-bottom: 16px;
+
+          :deep(img) {
+            max-width: 100%;
+          }
         }
 
         .q_options {

+ 7 - 1
src/views/teachingHomework/homeworkTask/list.vue

@@ -119,6 +119,10 @@ export default {
     },
   },
   mounted() {
+    // 从「查看作答 / 查看分析」返回时带 status=1,列表默认展示「已提交」
+    if (Number(this.$route.query.status) === 1) {
+      this.submitStatus = 1;
+    }
     this.fetchList();
   },
   methods: {
@@ -176,13 +180,15 @@ export default {
     applyPage(res) {
       const page = (res && res.data) || {};
       this.list = page.records || [];
-      this.total = page.total || 0;
+      this.total = Number(page.total) || 0;
     },
     homeworkPayload(item) {
       return {
         id: item.homeWorkStuId || item.homeWorkId,
         homeWorkId: item.homeWorkId,
         homeWorkStuId: item.homeWorkStuId,
+        // 班级教辅任务ID,查看分析接口使用;列表若直接返回则优先取
+        aidPaperClassId: item.aidPaperClassId || item.homeWorkId,
         subjectName: item.courseName || this.subjectName || '',
         courseCode: item.courseCode || this.subjectCode,
         homeworkName: item.homeWorkName || '',

+ 203 - 72
src/views/teachingHomework/homeworkTask/viewAnswer.vue

@@ -19,7 +19,7 @@
     </div>
 
     <!-- 主体:左侧试题序号,右侧题干/作答/解析 -->
-    <div class="answer_body">
+    <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">
@@ -60,8 +60,8 @@
               <span class="q_type">{{ currentQuestion.typeName }}</span>
               <span class="q_level">{{ currentQuestion.difficulty }}</span>
             </div>
-            <div class="q_stem">{{ currentQuestion.stem }}</div>
-            <div v-if="currentQuestion.type === 'single'" class="q_options">
+            <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>
@@ -114,11 +114,11 @@
                   </span>
                 </div>
               </div>
-              <div class="analysis_row">
-                <span class="analysis_label">【 答 案 】</span>
+              <div class="analysis_row" v-if="hasAnalysis">
+                <span class="analysis_label" >【 答 案 】</span>
                 <div class="analysis_content">
                   <span v-if="isSingle" class="correct_answer">{{ currentQuestion.correctAnswer }}</span>
-                  <div class="analysis_text">{{ currentQuestion.analysis }}</div>
+                  <div class="analysis_text" v-html="currentQuestion.analysis"></div>
                 </div>
               </div>
             </div>
@@ -144,6 +144,9 @@
           </div>
         </div>
       </div>
+      <div v-else-if="!loading" class="empty_panel no_content_data">
+        <span>暂无数据</span>
+      </div>
     </div>
 
     <el-image-viewer
@@ -161,49 +164,58 @@ import ElImageViewer from 'element-ui/packages/image/src/image-viewer';
 // 查看作答状态存 sessionStorage,刷新仍停留本页,地址栏不带参数
 const STORAGE_KEY = 'teachingHomeworkViewAnswer';
 
+const OPTION_KEYS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+// 客观题(选择题/判断题)渲染为单选项,其余按主观题作答图片展示
+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 mapKnowledge(list) {
+  if (!Array.isArray(list)) return [];
+  return list.map(item => {
+    if (item == null) return '';
+    if (typeof item === 'string') return item;
+    return item.knowledgeName || item.name || item.title || '';
+  }).filter(Boolean);
+}
+
 /**
- * 临时试题 + 作答/解析数据,接口就绪后替换
- * type: single 选择题 / photo 解答题
+ * 接口试题数据映射为页面结构
+ * type: single 客观题 / photo 主观题
  */
-function buildQuestions() {
-  const options = [
-    { key: 'A', text: '①⑤⑨' },
-    { key: 'B', text: '①③⑦' },
-    { key: 'C', text: '④⑤⑥' },
-    { key: 'D', text: '③④⑧' },
-  ];
-  const knowledgePoints = ['作家作品', '分析体裁特征和表现手法', '阅读理解'];
-  const list = [];
-  for (let i = 1; i <= 11; i += 1) {
-    list.push({
-      id: i,
-      type: 'single',
-      typeName: '选择题',
-      groupName: '单选题',
-      difficulty: '容易',
-      stem: `${i}、下列各句中,加点词的意义和用法相同的一组是`,
-      options,
-      userAnswer: i % 4 === 0 ? 'B' : 'A',
-      correctAnswer: 'D',
-      knowledgePoints,
-      analysis: '本题考查文言实词的意义和用法。结合语境分析各选项中加点词含义,正确答案为 D。',
-    });
-  }
-  for (let i = 12; i <= 16; i += 1) {
-    list.push({
-      id: i,
-      type: 'photo',
-      typeName: '解答题',
-      groupName: '解答题',
-      difficulty: '中等',
-      stem: `${i}、阅读材料,结合课文内容简要分析,并写出你的理解。`,
-      userAnswer: [],
-      correctAnswer: '',
-      knowledgePoints,
-      analysis: '解答本题需先通读材料,抓住关键信息,结合课文主旨作答,注意条理清晰。',
-    });
-  }
-  return list;
+function mapQuestion(item, index) {
+  const choice = isChoiceQuestion(item);
+  const id = item.homeWorkStuQuesId || item.questionId || index;
+  return {
+    id,
+    questionId: item.questionId,
+    homeWorkStuQuesId: item.homeWorkStuQuesId,
+    type: choice ? 'single' : 'photo',
+    typeName: item.questionTypeName || (choice ? '客观题' : '主观题'),
+    groupName: item.questionTypeName || (choice ? '客观题' : '主观题'),
+    difficulty: item.difficultyName || '',
+    stem: item.questionContent || '',
+    options: choice ? buildOptions(item) : [],
+    // 客观题取 ABCD 答案,主观题取作答图片地址
+    userAnswer: choice
+      ? (item.writeAnswer || '')
+      : (Array.isArray(item.writeUrls) ? item.writeUrls : []),
+    correctAnswer: item.questionAnswer || '',
+    knowledgePoints: mapKnowledge(item.knowledge),
+    analysis: item.questionAnalysis || '',
+    submitEndTime: item.submitEndTime || '',
+  };
 }
 
 export default {
@@ -214,7 +226,8 @@ export default {
   data() {
     return {
       homework: null,
-      questions: buildQuestions(),
+      questions: [],
+      loading: false,
       currentIndex: 0,
       activeSection: 'stem',
       sectionTabs: [
@@ -225,6 +238,9 @@ export default {
       imgVisible: false,
       previewIndex: 0,
       scrollLock: false,
+      scrollTimer: null,
+      // 点击 tab 滚动结束后的短暂宽限期,忽略残余 scroll 事件对高亮的覆盖
+      syncSuppressUntil: 0,
     };
   },
   computed: {
@@ -259,6 +275,20 @@ export default {
     isSingle() {
       return !!(this.currentQuestion && this.currentQuestion.type === 'single');
     },
+    hasOptionText() {
+      const options = (this.currentQuestion && this.currentQuestion.options) || [];
+      return options.some(opt => opt.text);
+    },
+    // 解析为 HTML,空串或 <p><br></p> 这类空标签都视为无内容;仅含公式图片也算有内容
+    hasAnalysis() {
+      const raw = String((this.currentQuestion && this.currentQuestion.analysis) || '');
+      if (/<img[^>]+>/i.test(raw)) return true;
+      const text = raw
+        .replace(/<[^>]*>/g, '')
+        .replace(/&nbsp;/g, '')
+        .trim();
+      return text !== '';
+    },
     isLastQuestion() {
       return this.currentIndex >= this.questions.length - 1;
     },
@@ -276,6 +306,7 @@ export default {
     this.restoreState();
   },
   beforeDestroy() {
+    clearInterval(this.scrollTimer);
     this.persistState();
   },
   methods: {
@@ -293,10 +324,50 @@ export default {
       }
       this.homework = cache.homework;
       this.currentIndex = Number(cache.currentIndex) || 0;
+      this.activeSection = 'stem';
+      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;
+      });
+    },
+    // 公共请求把 code 为 0 的成功响应放进了 reject,这里两种都认
+    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 list = Array.isArray(res && res.data) ? res.data : [];
+      this.questions = list.map(mapQuestion);
       if (this.currentIndex < 0 || this.currentIndex >= this.questions.length) {
         this.currentIndex = 0;
       }
       this.activeSection = 'stem';
+      this.persistState();
       this.$nextTick(() => {
         const box = this.$refs.scrollBox;
         if (box) box.scrollTop = 0;
@@ -315,6 +386,9 @@ export default {
       };
     },
     selectQuestion(index) {
+      clearInterval(this.scrollTimer);
+      this.scrollTimer = null;
+      this.scrollLock = false;
       this.currentIndex = index;
       this.activeSection = 'stem';
       this.persistState();
@@ -331,37 +405,74 @@ export default {
       if (this.currentIndex >= this.questions.length - 1) return;
       this.selectQuestion(this.currentIndex + 1);
     },
+    getSections() {
+      return [
+        { key: 'stem', el: this.$refs.sectionStem },
+        { key: 'answer', el: this.$refs.sectionAnswer },
+        { key: 'analysis', el: this.$refs.sectionAnalysis },
+      ];
+    },
     // 点击 tab 滚动到对应区块
     scrollToSection(key) {
-      const map = {
-        stem: this.$refs.sectionStem,
-        answer: this.$refs.sectionAnswer,
-        analysis: this.$refs.sectionAnalysis,
-      };
-      const target = map[key];
       const box = this.$refs.scrollBox;
-      if (!target || !box) return;
+      const target = this.getSections().find(item => item.key === key);
+      const targetEl = target && target.el;
+      if (!targetEl || !box) return;
       this.activeSection = key;
       this.scrollLock = true;
-      box.scrollTop = target.offsetTop - 8;
-      setTimeout(() => {
-        this.scrollLock = false;
-      }, 300);
+      // 用视口差值 + 当前滚动距离计算目标位置,offsetTop 受 offsetParent 影响会不准
+      const distance = targetEl.getBoundingClientRect().top - box.getBoundingClientRect().top;
+      // 下方区块高度不足时,目标顶部无法对齐容器顶部,最多只能滚到底部
+      const maxScrollTop = box.scrollHeight - box.clientHeight;
+      const targetTop = Math.max(0, Math.min(box.scrollTop + distance - 8, maxScrollTop));
+      if (typeof box.scrollTo === 'function') {
+        box.scrollTo({ top: targetTop, behavior: 'smooth' });
+      } else {
+        box.scrollTop = targetTop;
+      }
+      clearInterval(this.scrollTimer);
+      // 平滑滚动期间锁定 onScroll 的高亮覆盖;连续两帧位置不变视为滚动结束
+      let lastTop = box.scrollTop;
+      let stableTicks = 0;
+      this.scrollTimer = setInterval(() => {
+        const nowTop = box.scrollTop;
+        if (Math.abs(nowTop - lastTop) < 1) {
+          stableTicks += 1;
+        } else {
+          stableTicks = 0;
+          lastTop = nowTop;
+        }
+        if (stableTicks >= 2) {
+          clearInterval(this.scrollTimer);
+          this.scrollTimer = null;
+          // 保持用户点击的 tab:下方高度不足停在底部时,按位置重算会误判到后一个区块
+          this.activeSection = key;
+          // 解锁后宽限 150ms,吞掉滚动停止瞬间的残余 scroll 事件
+          this.syncSuppressUntil = Date.now() + 150;
+          this.scrollLock = false;
+        }
+      }, 80);
     },
     // 滚动时同步高亮 tab
     onScroll() {
       if (this.scrollLock) return;
+      if (this.syncSuppressUntil && Date.now() < this.syncSuppressUntil) return;
+      this.syncActiveSection();
+    },
+    // 依据各区块相对滚动容器的位置计算当前高亮;滚到底部时高亮最后一个可见区块
+    syncActiveSection() {
       const box = this.$refs.scrollBox;
       if (!box) return;
-      const scrollTop = box.scrollTop;
-      const sections = [
-        { key: 'stem', el: this.$refs.sectionStem },
-        { key: 'answer', el: this.$refs.sectionAnswer },
-        { key: 'analysis', el: this.$refs.sectionAnalysis },
-      ];
+      const boxTop = box.getBoundingClientRect().top;
+      const reachedBottom = box.scrollTop + box.clientHeight >= box.scrollHeight - 2;
       let current = 'stem';
-      sections.forEach(item => {
-        if (item.el && item.el.offsetTop - 24 <= scrollTop) {
+      this.getSections().forEach(item => {
+        if (!item.el) return;
+        const rect = item.el.getBoundingClientRect();
+        if (rect.top - boxTop <= 24) {
+          current = item.key;
+        }
+        if (reachedBottom && rect.bottom > boxTop) {
           current = item.key;
         }
       });
@@ -379,7 +490,8 @@ export default {
     },
     goBack() {
       this.persistState();
-      this.$router.push('/teachingHomework/list');
+      // 从查看作答返回时,列表定位到「已提交」页签
+      this.$router.push({ path: '/teachingHomework/list', query: { status: 1 } });
     },
   },
 };
@@ -475,6 +587,14 @@ export default {
     margin-top: 10px;
     overflow: hidden;
 
+    .empty_panel {
+      flex: 1;
+      min-width: 0;
+      height: 100%;
+      background: #fff;
+      border-radius: 10px;
+    }
+
     .question_nav {
       flex-shrink: 0;
       width: 240px;
@@ -547,7 +667,7 @@ export default {
         flex-shrink: 0;
         display: flex;
         align-items: center;
-        gap: 32px;
+        gap: 20px;
         padding:16px;
         box-sizing: border-box;
 
@@ -587,6 +707,9 @@ export default {
 
         .content_section {
           margin-bottom: 20px;
+          &:last-child {
+            margin-bottom: 0;
+          }
         }
 
         .q_tags {
@@ -628,7 +751,10 @@ export default {
           font-size: 15px;
           line-height: 1.7;
           color: #333;
-          margin-bottom: 16px;
+
+          :deep(img) {
+            max-width: 100%;
+          }
         }
 
         .q_options {
@@ -699,8 +825,8 @@ export default {
               background: #fff;
 
               &::after {
-                width: 8px;
-                height: 8px;
+                width: 10px;
+                height: 10px;
                 background-color: #2E64FA;
                 border-radius: 50%;
               }
@@ -824,6 +950,11 @@ export default {
               font-size: 14px;
               line-height: 1.7;
               color: #666;
+
+              :deep(img) {
+                max-width: 100%;
+                vertical-align: middle;
+              }
             }
           }
         }