Gqingci 1 هفته پیش
والد
کامیت
c8d99b2e24

+ 11 - 0
src/api/main/postManage/candidate.ts

@@ -11,6 +11,7 @@ export interface PostCandidateVO {
   id: string | number;
   postId?: string | number;
   studentId?: string | number;
+  evaluationId?: string | number;
   name?: string;
   gender?: string;
   phone?: string;
@@ -57,3 +58,13 @@ export function hirePostCandidate(data: PostCandidateHireForm) {
     data
   });
 }
+
+export function downloadResume(studentId: string | number) {
+  return import('@/utils/request').then(({ default: req }) => {
+    return req({
+      url: `/main/student/downloadResume/${studentId}`,
+      method: 'get',
+      responseType: 'blob'
+    });
+  });
+}

+ 0 - 1
src/views/evaluation/apply-list.vue

@@ -27,7 +27,6 @@
             </template>
           </el-table-column>
           <el-table-column label="性别" prop="gender" width="80" align="center" />
-          <el-table-column label="部门" prop="department" width="100" align="center" />
           <el-table-column label="手机号" prop="phone" min-width="160" align="center" />
           <el-table-column label="状态" min-width="160" align="center">
             <template #default="{ row }">

+ 2 - 2
src/views/evaluation/components/EvaluationFilter.vue

@@ -12,8 +12,8 @@
     </el-form-item>
     <el-form-item prop="status" class="filter-item">
       <el-select :model-value="status" placeholder="状态" clearable @update:model-value="emit('update:status', $event || '')">
-        <el-option label="启用" value="0" />
-        <el-option label="停用" value="1" />
+        <el-option label="启用" value="1" />
+        <el-option label="停用" value="2" />
       </el-select>
     </el-form-item>
     <el-form-item class="filter-item filter-item--date">

+ 5 - 4
src/views/evaluation/components/EvaluationTable.vue

@@ -5,7 +5,7 @@
         <el-checkbox :model-value="isAllChecked" :indeterminate="isIndeterminate" @change="handleCheckAllChange" />
       </template>
       <template #default="{ row }">
-        <el-checkbox :model-value="isRowChecked(row)" @change="(checked) => handleRowCheckedChange(row, checked)" />
+        <el-checkbox :model-value="isRowChecked(row)" :disabled="row.delFlag === '1'" @change="(checked) => handleRowCheckedChange(row, checked)" />
       </template>
     </el-table-column>
     <el-table-column label="测评图片" width="88" align="center">
@@ -28,16 +28,17 @@
     </el-table-column>
     <el-table-column label="参与数" min-width="130" align="center">
       <template #default="{ row }">
-        <el-button link type="primary" @click="emit('viewApplyList', row)">{{ Number(row.participantCount || 0) }}/{{ Number(row.totalCount || 0) }}</el-button>
+        <el-button link type="primary" :disabled="row.delFlag === '1'" @click="emit('viewApplyList', row)">{{ Number(row.participantCount || 0) }}/{{ Number(row.totalCount || 0) }}</el-button>
       </template>
     </el-table-column>
     <el-table-column label="状态" width="110" align="center">
       <template #default="{ row }">
         <el-switch
-          :model-value="row.status === '0'"
+          :model-value="row.status === '1'"
           inline-prompt
           active-text="开"
           inactive-text="关"
+          :disabled="row.delFlag === '1'"
           :loading="switchLoadingMap[String(row.id)]"
           @change="emit('toggleStatus', row, $event)"
         />
@@ -63,7 +64,7 @@
 
     <el-table-column label="操作" width="120" align="center" fixed="right">
       <template #default="{ row }">
-        <el-button link type="primary" @click="emit('viewApplyList', row)">查看</el-button>
+        <el-button link type="primary" :disabled="row.delFlag === '1'" @click="emit('viewApplyList', row)">查看</el-button>
       </template>
     </el-table-column>
   </el-table>

+ 341 - 108
src/views/evaluation/components/evaluation-view.vue

@@ -6,6 +6,9 @@
         <div class="info-list">
           <div class="info-item"><span>姓名:</span><span>{{ studentName }}</span></div>
           <div class="info-item"><span>报名岗位:</span><span>{{ positionName }}</span></div>
+          <div class="info-item"><span>开始时间:</span><span>{{ startTime || '-' }}</span></div>
+          <div class="info-item"><span>结束时间:</span><span>{{ commitTime || '-' }}</span></div>
+          <div class="info-item"><span>答题时长:</span><span>{{ ansTime || '-' }}</span></div>
         </div>
 
         <div class="card-title second">答题信息</div>
@@ -18,7 +21,9 @@
         </div>
 
         <div class="card-title second">维度分析</div>
-        <div class="chart-placeholder">暂不支持</div>
+        <div v-if="scores.length >= 3" ref="radarChartRef" class="radar-chart"></div>
+        <div v-else class="chart-placeholder">维度数据不足,至少需要3项能力</div>
+
         <div class="footer-btn">
           <el-button @click="handleBack">返回</el-button>
         </div>
@@ -28,7 +33,7 @@
         <template v-if="questionList.length > 0">
           <div v-for="(item, index) in questionList" :key="item.id" class="question-block">
             <div class="question-head">
-              <span>第{{ index + 1 }}题</span>
+              <span>第{{ index + 1 }}题 <el-tag size="small" type="info">{{ item.typeName }}</el-tag></span>
               <div class="question-result">
                 <el-tag :type="item.correct ? 'success' : 'danger'">{{ item.correct ? '正确' : '错误' }}</el-tag>
                 <span>得分</span>
@@ -36,12 +41,25 @@
               </div>
             </div>
             <div class="question-title" v-html="item.questionTitle"></div>
-            <div class="question-options">
-              <div v-for="option in item.options" :key="option.label" class="question-option">
-                <el-radio :model-value="item.answer" :label="option.label">{{ option.label }}.<span v-html="option.text"></span></el-radio>
-                <span v-if="option.correct" class="correct-text">正确答案</span>
+            <!-- 选择题:显示选项,标记用户选择和正确答案 -->
+            <div v-if="item.options && item.options.length > 0" class="question-options">
+              <div v-for="option in item.options" :key="option.label" class="question-option" :class="{ 'is-user-answer': item.answer && item.answer.includes(option.label) }">
+                <!-- 单选题 -->
+                <el-radio v-if="item.typeName === '单选题'" :model-value="item.answer" :label="option.label" disabled>{{ option.label }}.<span v-html="option.text"></span></el-radio>
+                <!-- 多选题 -->
+                <el-checkbox v-else :model-value="item.answer ? item.answer.includes(option.label) : false" :label="option.label" disabled>{{ option.label }}.<span v-html="option.text"></span></el-checkbox>
+                <el-tag v-if="option.correct" size="small" type="success">正确答案</el-tag>
+                <el-tag v-if="item.answer && item.answer.includes(option.label) && !option.correct" size="small" type="danger">考生选择</el-tag>
               </div>
             </div>
+            <!-- 问答题:显示考生作答 -->
+            <div v-if="item.answer && (!item.options || item.options.length === 0)" class="question-user-answer">
+              <span>考生作答:</span><span v-html="item.answer"></span>
+            </div>
+            <!-- 正确答案(问答题) -->
+            <div v-if="item.typeName === '问答题' && item.testAnsRight" class="question-right-answer">
+              <span>参考答案:</span><span v-html="item.testAnsRight"></span>
+            </div>
             <div v-if="item.analysis" class="question-analysis">
               <div>答案解析:</div>
               <div v-html="item.analysis"></div>
@@ -54,9 +72,10 @@
   </PageShell>
 </template>
 
-<script setup name="PostManageEvaluationView" lang="ts">
-import { getCurrentInstance, type ComponentInternalInstance, ref, onMounted } from 'vue';
+<script setup name="EvaluationView" lang="ts">
+import { getCurrentInstance, type ComponentInternalInstance, ref, onMounted, nextTick, watch } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
+import * as echarts from 'echarts';
 import PageShell from '@/components/PageShell/index.vue';
 import { getEvaluation, getExamScoreList, getExamPaperQuestions, getExamAnswerList } from '@/api/main/evaluation/index';
 
@@ -69,129 +88,304 @@ const evaluationId = route.query.evaluationId as string;
 const studentId = route.query.studentId as string;
 const studentName = ref((route.query.name as string) || '');
 const positionName = ref('');
+const startTime = ref('');
+const commitTime = ref('');
+const ansTime = ref('');
 
 const scores = ref<any[]>([]);
 const questionList = ref<any[]>([]);
-
-const parseJson = (str: any) => {
-  if (typeof str !== 'string') return str || {};
-  try { return JSON.parse(str); } catch (e) { return {}; }
+const radarChartRef = ref<HTMLElement | null>(null);
+let radarChart: echarts.ECharts | null = null;
+
+/** 解析接口返回的 msg 字段(JSON 字符串),提取 bizContent */
+const parseBizContent = (res: any) => {
+  const msgStr = res?.msg;
+  if (typeof msgStr !== 'string') return {};
+  try {
+    const parsed = JSON.parse(msgStr);
+    return parsed?.bizContent || parsed || {};
+  } catch (e) {
+    return {};
+  }
 };
 
-const findStudentInList = (data: any, sid: string, sname: string) => {
-  let list = Array.isArray(data) ? data : (data?.data?.list || data?.list || data?.data || data?.rows || data?.records || []);
-  if (!Array.isArray(list)) {
-      if(data && typeof data === 'object' && Object.keys(data).length > 0) {
-          if (sid && (String(data.userId) === String(sid) || String(data.user_id) === String(sid))) {
-              return data;
-          }
-      }
-      return null;
+/** 从 score-list 的 bizContent.rows 中查找指定学生的最新记录 */
+const findStudentInScoreList = (bizContent: any, sid: string) => {
+  const rows: any[] = bizContent?.rows || [];
+  if (!Array.isArray(rows) || !sid) return null;
+  // 同一考生可能有多条记录,取最后一条(最新的)
+  let result: any = null;
+  for (const item of rows) {
+    if (String(item.userId) === String(sid)) {
+      result = item;
+    }
   }
-  return list.find((item: any) => 
-    (sid && (String(item.userId) === String(sid) || String(item.user_id) === String(sid) || String(item.studentId) === String(sid))) || 
-    (sname && (String(item.userName) === String(sname) || String(item.user_name) === String(sname) || String(item.name) === String(sname)))
-  );
+  return result;
 };
 
-const extractQuestions = (data: any) => {
-  let qlist = Array.isArray(data) ? data : (data?.data?.questions || data?.data?.questionList || data?.questions || data?.questionList || data?.data || []);
-  return Array.isArray(qlist) ? qlist : [];
+/** 将 com_ans 中的 key1~key4 转为 A/B/C/D 标签 */
+const convertComAnsToLabel = (comAns: string): string => {
+  if (!comAns) return '';
+  const keyMap: Record<string, string> = { key1: 'A', key2: 'B', key3: 'C', key4: 'D' };
+  // com_ans 格式可能是 "key2," 或 "key1,key3," 等
+  const keys = comAns.split(',').map((k: string) => k.trim()).filter(Boolean);
+  return keys.map((k: string) => keyMap[k] || k).join('');
 };
 
-const extractAnswers = (ansData: any) => {
-    let answers = ansData?.details || ansData?.answerList || ansData?.answers || ansData?.questionList || ansData?.data || [];
-    return Array.isArray(answers) ? answers : [];
+const TYPE_NAME_MAP: Record<number, string> = {
+  1: '单选题',
+  2: '多选题',
+  3: '判断题',
+  5: '问答题'
 };
 
-const combineQA = (questions: any[], answers: any[]) => {
+/** 从 paper-questions 的 bizContent.rows 中提取题目列表
+ *  注意:paper-questions 的 testId 是 "--",真实的 test_id 在 answer-list 中
+ *  所以这里需要用 answer-list 中的信息来确定题目 ID
+ */
+const extractQuestions = (bizContent: any, ansMap: Map<string, any>) => {
+  const rows: any[] = bizContent?.rows || [];
+  if (!Array.isArray(rows)) return [];
+
+  // 建立 question 文本 -> answer-map 中 test_id 的映射
+  const questionToTestId = new Map<string, string>();
+  ansMap.forEach((detail: any, testId: string) => {
+    const q = detail.question || '';
+    if (q) {
+      questionToTestId.set(q, testId);
+    }
+  });
+
+  const questions: any[] = [];
+  let seq = 0;
+  rows.forEach((row: any) => {
+    const tc = row.testContent || {};
+    const qType = Number(tc.type);
+    const questionTitle = tc.question || '';
+    const analysis = tc.analysis || '';
+
+    // 优先用 answer-list 中的 test_id 匹配
+    let questionId = questionToTestId.get(questionTitle);
+    if (!questionId) {
+      questionId = tc.test_id || row.testId || `q_${seq}`;
+    }
+
+    // 构建选项:问答题无选项,单选/多选题从 answer1~answer4 + key1~key4 构建
+    const options: { label: string; text: string; correct: boolean }[] = [];
+    if (qType === 1 || qType === 2) {
+      const answerFields = ['answer1', 'answer2', 'answer3', 'answer4'];
+      const keyFields = ['key1', 'key2', 'key3', 'key4'];
+      const labels = ['A', 'B', 'C', 'D'];
+      for (let i = 0; i < 4; i++) {
+        const text = tc[answerFields[i]];
+        if (text) {
+          options.push({
+            label: labels[i],
+            text,
+            correct: tc[keyFields[i]] === '1'
+          });
+        }
+      }
+    }
+
+    questions.push({
+      id: questionId,
+      questionTitle,
+      analysis,
+      options,
+      type: qType,
+      typeName: TYPE_NAME_MAP[qType] || '其他',
+      testName: row.testName || ''
+    });
+    seq++;
+  });
+  return questions;
+};
+
+/** 从 answer-list 的 bizContent.rows 中查找指定学生,并提取其作答信息 */
+const extractAnswers = (bizContent: any, sid: string) => {
+  const rows: any[] = bizContent?.rows || [];
+  if (!Array.isArray(rows) || !sid) return new Map<string, any>();
+  const studentRow = rows.find((item: any) => String(item.userId) === String(sid));
+  if (!studentRow?.ansAndScore) return new Map<string, any>();
+
+  const ansMap = new Map<string, any>();
+  const ansAndScore: any[] = studentRow.ansAndScore;
+
+  for (const group of ansAndScore) {
+    if (!Array.isArray(group)) continue;
+    for (let i = 1; i < group.length; i++) {
+      const entry = group[i];
+      if (Array.isArray(entry) && entry.length >= 2) {
+        const testId = String(entry[0]);
+        const detail = entry[1] || {};
+        ansMap.set(testId, detail);
+      }
+    }
+  }
+  return ansMap;
+};
+
+/** 将题目和作答合并为展示数据 */
+const combineQA = (questions: any[], ansMap: Map<string, any>) => {
   return questions.map((q, index) => {
-    let ans = answers.find(a => String(a.questionId) === String(q.id) || String(a.question_id) === String(q.id) || String(a.id) === String(q.id));
-    
-    let optionsObj = q.options || q.qOption || q.questionOptions || [];
-    if (typeof optionsObj === 'string') {
-      try { optionsObj = JSON.parse(optionsObj); } catch(e) {}
+    const ans = ansMap.get(String(q.id));
+
+    let correct = false;
+    if (ans) {
+      if (ans.is_ok === 'right') correct = true;
     }
-    
-    let formattedOptions = Array.isArray(optionsObj) ? optionsObj.map((o: any, idx: number) => {
-        return {
-            label: o.label || o.key || o.option || String.fromCharCode(65 + idx),
-            text: o.text || o.content || o.value || o.optionDesc || '',
-            correct: !!o.correct || !!o.isRight || !!o.isCorrect
-        };
-    }) : [];
-
-    let c = false;
+
+    // 用户答案
+    let answer = '';
     if (ans) {
-        if(ans.isCorrect !== undefined) c = !!ans.isCorrect;
-        else if (ans.correct !== undefined) c = !!ans.correct;
-        else if (Number(ans.score) > 0) c = true;
+      const comAns = ans.com_ans || '';
+      if (comAns && (q.type === 1 || q.type === 2)) {
+        // 选择题:将 key1~key4 转为 A~D
+        answer = convertComAnsToLabel(comAns);
+      } else if (comAns) {
+        // 问答题:直接使用 HTML 内容
+        answer = comAns;
+      }
     }
 
+    // 正确答案文本(问答题用)
+    const testAnsRight = ans?.test_ans_right || '';
+
+    const options = q.options.map((o: any) => ({ ...o }));
+
     return {
-       id: q.id || `q_${index}_${Math.random().toString(36).substring(2, 6)}`,
-       questionTitle: q.title || q.content || q.questionContent || '未命名题目',
-       correct: c,
-       score: ans?.score || 0,
-       answer: ans?.userAnswer || ans?.answer || ans?.myAnswer || '',
-       options: formattedOptions.length > 0 ? formattedOptions : [ { label: 'A', text: '选项A' }, { label: 'B', text: '选项B' } ],
-       analysis: q.analysis || q.answerAnalysis || q.qAnalysis || q.questionAnalysis || ''
+      id: q.id || `q_${index}`,
+      questionTitle: q.questionTitle || '未命名题目',
+      correct,
+      score: ans?.score ?? 0,
+      answer,
+      options: options.length > 0 ? options : q.type === 5 ? [] : [{ label: 'A', text: '选项A' }, { label: 'B', text: '选项B' }],
+      analysis: q.analysis || '',
+      typeName: q.typeName,
+      testName: q.testName,
+      testAnsRight
     };
   });
 };
 
-const loadData = async () => {
-    if(!evaluationId) return;
-    loading.value = true;
-    try {
-        const res = await getEvaluation(evaluationId);
-        const evalData = res.data;
-        positionName.value = evalData.position || '未知岗位';
-
-        const abilityConfigs = evalData.abilityConfigs || [];
-        let allQuestions: any[] = [];
-        let scoreItems: any[] = [];
-
-        for (const ability of abilityConfigs) {
-            const examId = ability.thirdExamInfoId;
-            if(!examId) continue;
-
-            const scoreRes = await getExamScoreList({ examInfoId: examId, page: 1 }).catch(()=>({data: {}}));
-            const scoreData = parseJson(scoreRes.data);
-            const myScoreObj = findStudentInList(scoreData, studentId, studentName.value);
-            
-            const score = myScoreObj?.totalScore || myScoreObj?.score || myScoreObj?.examScore || 0;
-            const passMark = ability.thirdExamPassMark || 0;
-            const pass = Number(score) >= Number(passMark);
-
-            scoreItems.push({
-                abilityName: ability.abilityName || ability.thirdExamName || '考核',
-                score: score,
-                totalScore: ability.thirdExamTotalScore || 100,
-                pass
-            });
-
-            const paperRes = await getExamPaperQuestions({ examInfoId: examId }).catch(()=>({data: {}}));
-            const paperData = parseJson(paperRes.data);
-            const questions = extractQuestions(paperData);
-
-            const answerRes = await getExamAnswerList({ examInfoId: examId }).catch(()=>({data: {}}));
-            const answerData = parseJson(answerRes.data);
-            const myAnswerObj = findStudentInList(answerData, studentId, studentName.value);
-            const answers = myAnswerObj ? extractAnswers(myAnswerObj) : [];
-
-            allQuestions.push(...combineQA(questions, answers));
+/** 渲染雷达图 */
+const renderRadarChart = () => {
+  if (!radarChartRef.value || scores.value.length < 3) return;
+
+  if (!radarChart) {
+    radarChart = echarts.init(radarChartRef.value);
+  }
+
+  const indicator = scores.value.map((s: any) => ({
+    name: s.abilityName,
+    max: Number(s.totalScore) || 100
+  }));
+
+  const dataValues = scores.value.map((s: any) => Number(s.score) || 0);
+
+  radarChart.setOption({
+    tooltip: {
+      trigger: 'item'
+    },
+    radar: {
+      indicator,
+      radius: '65%',
+      name: {
+        textStyle: {
+          fontSize: 11
+        }
+      },
+      splitArea: {
+        areaStyle: {
+          color: ['rgba(64,158,255,0.05)', 'rgba(64,158,255,0.1)', 'rgba(64,158,255,0.15)', 'rgba(64,158,255,0.2)']
         }
+      }
+    },
+    series: [{
+      type: 'radar',
+      data: [{
+        value: dataValues,
+        name: '得分',
+        areaStyle: {
+          color: 'rgba(64,158,255,0.2)'
+        },
+        lineStyle: {
+          color: '#409EFF'
+        },
+        itemStyle: {
+          color: '#409EFF'
+        }
+      }]
+    }]
+  });
+};
+
+const loadData = async () => {
+  if (!evaluationId) return;
+  loading.value = true;
+  try {
+    const res = await getEvaluation(evaluationId);
+    const evalData = res.data;
+    positionName.value = evalData.position || '未知岗位';
+
+    const abilityConfigs = evalData.abilityConfigs || [];
+    let allQuestions: any[] = [];
+    let scoreItems: any[] = [];
+
+    for (const ability of abilityConfigs) {
+      const examId = ability.thirdExamInfoId;
+      if (!examId) continue;
+
+      // 获取成绩列表
+      const scoreRes = await getExamScoreList({ examInfoId: examId, page: 1 }).catch(() => ({ msg: '{}' }));
+      const scoreBiz = parseBizContent(scoreRes);
+      const myScoreObj = findStudentInScoreList(scoreBiz, studentId);
+
+      const score = myScoreObj?.score || 0;
+      const passMark = ability.thirdExamPassMark || ability.passingScore || 0;
+      const pass = myScoreObj ? String(myScoreObj.isPass) === '1' : Number(score) >= Number(passMark);
+
+      // 取考生时间信息(只需取第一次有数据的记录)
+      if (myScoreObj && !startTime.value) {
+        startTime.value = myScoreObj.startTime || '';
+        commitTime.value = myScoreObj.commitTime || '';
+        ansTime.value = myScoreObj.ansTime || '';
+      }
 
-        scores.value = scoreItems;
-        questionList.value = allQuestions;
+      scoreItems.push({
+        abilityName: ability.abilityName || ability.thirdExamName || '考核',
+        score: score,
+        totalScore: ability.thirdExamTotalScore || ability.score || 100,
+        pass
+      });
 
-    } catch (e) {
-        console.error(e);
-        proxy?.$modal?.msgError('获取测评数据失败');
-    } finally {
-        loading.value = false;
+      // 获取答题详情(先获取,因为需要用 test_id 来匹配 paper-questions)
+      const answerRes = await getExamAnswerList({ examInfoId: examId }).catch(() => ({ msg: '{}' }));
+      const answerBiz = parseBizContent(answerRes);
+      const ansMap = extractAnswers(answerBiz, studentId);
+
+      // 获取试卷题目
+      const paperRes = await getExamPaperQuestions({ examInfoId: examId }).catch(() => ({ msg: '{}' }));
+      const paperBiz = parseBizContent(paperRes);
+      const questions = extractQuestions(paperBiz, ansMap);
+
+      allQuestions.push(...combineQA(questions, ansMap));
     }
+
+    scores.value = scoreItems;
+    questionList.value = allQuestions;
+
+    // 渲染雷达图
+    await nextTick();
+    renderRadarChart();
+
+  } catch (e) {
+    console.error(e);
+    proxy?.$modal?.msgError('获取测评数据失败');
+  } finally {
+    loading.value = false;
+  }
 };
 
 const handleBack = () => {
@@ -200,22 +394,31 @@ const handleBack = () => {
 };
 
 onMounted(() => {
-    loadData();
+  loadData();
 });
 </script>
 
 <style scoped>
 .evaluation-view-page {
   display: grid;
-  grid-template-columns: 180px 1fr;
+  grid-template-columns: 280px 1fr;
   gap: 16px;
 }
 
-.left-card,
+.left-card {
+  background: #fff;
+  border-radius: 6px;
+  padding: 16px;
+  height: calc(100vh - 180px);
+  overflow-y: auto;
+}
+
 .right-card {
   background: #fff;
   border-radius: 6px;
   padding: 16px;
+  height: calc(100vh - 180px);
+  overflow-y: auto;
 }
 
 .card-title {
@@ -252,6 +455,12 @@ onMounted(() => {
   align-items: center;
   justify-content: center;
   color: #909399;
+  font-size: 12px;
+}
+
+.radar-chart {
+  height: 220px;
+  margin-top: 8px;
 }
 
 .footer-btn {
@@ -298,6 +507,12 @@ onMounted(() => {
   display: flex;
   align-items: center;
   gap: 8px;
+  padding: 4px 8px;
+  border-radius: 4px;
+}
+
+.question-option.is-user-answer {
+  background: #ecf5ff;
 }
 
 .correct-text {
@@ -310,4 +525,22 @@ onMounted(() => {
   color: #606266;
   font-size: 13px;
 }
+
+.question-user-answer {
+  margin-top: 8px;
+  padding: 8px 12px;
+  background: #f5f7fa;
+  border-radius: 4px;
+  color: #606266;
+  font-size: 13px;
+}
+
+.question-right-answer {
+  margin-top: 8px;
+  padding: 8px 12px;
+  background: #f0f9eb;
+  border-radius: 4px;
+  color: #67c23a;
+  font-size: 13px;
+}
 </style>

+ 2 - 2
src/views/evaluation/index.vue

@@ -198,7 +198,7 @@ const handleQuery = () => {
 const resetQuery = () => {
   queryParams.evaluationName = '';
   queryParams.grade = '';
-  queryParams.position = '';
+  queryParams.positionId = '';
   queryParams.positionType = '';
   queryParams.status = '';
   dateRange.value = [];
@@ -211,7 +211,7 @@ const handleToggleStatus = async (row: MainExamEvaluationVO, value: boolean | st
   const rowKey = String(row.id);
   try {
     switchLoadingMap[rowKey] = true;
-    await updateEvaluationStatus(row.id, value ? '0' : '1');
+    await updateEvaluationStatus(row.id, value ? '1' : '2');
     modal?.msgSuccess('状态更新成功');
     await getList();
   } finally {

+ 2 - 2
src/views/evaluation/utils.ts

@@ -20,10 +20,10 @@ export const getImageUrl = (row: MainExamEvaluationVO) => {
 };
 
 export const getStatusMeta = (status?: string) => {
-  if (status === '0') {
+  if (status === '1') {
     return { label: '启用', type: 'success' as const };
   }
-  if (status === '1') {
+  if (status === '2') {
     return { label: '停用', type: 'info' as const };
   }
   return { label: status || '--', type: 'info' as const };

+ 34 - 4
src/views/postManage/apply-list.vue

@@ -57,7 +57,7 @@
             <template #default="scope">
               <template v-if="scope.row.status !== STATUS_CLOSED">
                 <el-button link type="primary" @click="handleEvaluationDetail(scope.row)">测评详情</el-button>
-                <el-button link type="primary">下载附件简历</el-button>
+                <el-button link type="primary" @click="handleDownloadResume(scope.row)">下载附件简历</el-button>
                 <template v-if="scope.row.status === STATUS_PENDING">
                   <el-button link type="primary" @click="handleHire(scope.row)">录用</el-button>
                   <el-button link type="primary" @click="handleReject(scope.row)">不录用</el-button>
@@ -129,6 +129,10 @@ import { useRoute, useRouter } from 'vue-router';
 import { hirePostCandidate, listPostCandidates, updatePostCandidateStatus, type PostCandidateVO } from '@/api/main/postManage/candidate';
 import PageShell from '@/components/PageShell/index.vue';
 import FileUpload from '@/components/FileUpload/index.vue';
+import FileSaver from 'file-saver';
+import { blobValidate } from '@/utils/ruoyi';
+import axios from 'axios';
+import { globalHeaders } from '@/utils/request';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const router = useRouter();
@@ -301,18 +305,44 @@ const handleOpenDetail = (row: { id: number; name: string }) => {
 };
 
 const handleEvaluationDetail = (row: PostCandidateVO) => {
+  if (!row.evaluationId) {
+    modal?.msgWarning('该岗位暂无关联测评');
+    return;
+  }
   router.push({
-    path: '/postManage/evaluation-view',
+    path: '/evaluation/evaluation-view',
     query: {
       applyId: String(row.id),
       name: row.name || '',
-      postId: String(route.query.postId || ''),
       studentId: String(row.studentId || ''),
-      evaluationId: String((row as any).evaluationId || route.query.postId || '')
+      evaluationId: String(row.evaluationId)
     }
   });
 };
 
+const baseURL = import.meta.env.VITE_APP_BASE_API;
+
+const handleDownloadResume = async (row: PostCandidateVO) => {
+  try {
+    const res = await axios({
+      method: 'get',
+      url: baseURL + `/main/student/downloadResume/${row.studentId}`,
+      responseType: 'blob',
+      headers: globalHeaders()
+    });
+    const isBlob = blobValidate(res.data);
+    if (isBlob) {
+      const blob = new Blob([res.data], { type: 'application/octet-stream' });
+      const fileName = decodeURIComponent(res.headers['download-filename'] as string || `${row.name || '简历'}_附件`);
+      FileSaver.saveAs(blob, fileName);
+    } else {
+      modal?.msgWarning('该候选人暂无简历附件');
+    }
+  } catch {
+    modal?.msgWarning('该候选人暂无简历附件');
+  }
+};
+
 onMounted(() => {
   getList();
 });

+ 307 - 109
src/views/postManage/evaluation-view.vue

@@ -6,6 +6,9 @@
         <div class="info-list">
           <div class="info-item"><span>姓名:</span><span>{{ studentName }}</span></div>
           <div class="info-item"><span>报名岗位:</span><span>{{ positionName }}</span></div>
+          <div class="info-item"><span>开始时间:</span><span>{{ startTime || '-' }}</span></div>
+          <div class="info-item"><span>结束时间:</span><span>{{ commitTime || '-' }}</span></div>
+          <div class="info-item"><span>答题时长:</span><span>{{ ansTime || '-' }}</span></div>
         </div>
 
         <div class="card-title second">答题信息</div>
@@ -18,7 +21,9 @@
         </div>
 
         <div class="card-title second">维度分析</div>
-        <div class="chart-placeholder">暂不支持</div>
+        <div v-if="scores.length >= 3" ref="radarChartRef" class="radar-chart"></div>
+        <div v-else class="chart-placeholder">维度数据不足,至少需要3项能力</div>
+
         <div class="footer-btn">
           <el-button @click="handleBack">返回</el-button>
         </div>
@@ -28,7 +33,7 @@
         <template v-if="questionList.length > 0">
           <div v-for="(item, index) in questionList" :key="item.id" class="question-block">
             <div class="question-head">
-              <span>第{{ index + 1 }}题</span>
+              <span>第{{ index + 1 }}题 <el-tag size="small" type="info">{{ item.typeName }}</el-tag></span>
               <div class="question-result">
                 <el-tag :type="item.correct ? 'success' : 'danger'">{{ item.correct ? '正确' : '错误' }}</el-tag>
                 <span>得分</span>
@@ -36,12 +41,20 @@
               </div>
             </div>
             <div class="question-title" v-html="item.questionTitle"></div>
-            <div class="question-options">
-              <div v-for="option in item.options" :key="option.label" class="question-option">
-                <el-radio :model-value="item.answer" :label="option.label">{{ option.label }}.<span v-html="option.text"></span></el-radio>
-                <span v-if="option.correct" class="correct-text">正确答案</span>
+            <div v-if="item.options && item.options.length > 0" class="question-options">
+              <div v-for="option in item.options" :key="option.label" class="question-option" :class="{ 'is-user-answer': item.answer && item.answer.includes(option.label) }">
+                <el-radio v-if="item.typeName === '单选题'" :model-value="item.answer" :label="option.label" disabled>{{ option.label }}.<span v-html="option.text"></span></el-radio>
+                <el-checkbox v-else :model-value="item.answer ? item.answer.includes(option.label) : false" :label="option.label" disabled>{{ option.label }}.<span v-html="option.text"></span></el-checkbox>
+                <el-tag v-if="option.correct" size="small" type="success">正确答案</el-tag>
+                <el-tag v-if="item.answer && item.answer.includes(option.label) && !option.correct" size="small" type="danger">考生选择</el-tag>
               </div>
             </div>
+            <div v-if="item.answer && (!item.options || item.options.length === 0)" class="question-user-answer">
+              <span>考生作答:</span><span v-html="item.answer"></span>
+            </div>
+            <div v-if="item.typeName === '问答题' && item.testAnsRight" class="question-right-answer">
+              <span>参考答案:</span><span v-html="item.testAnsRight"></span>
+            </div>
             <div v-if="item.analysis" class="question-analysis">
               <div>答案解析:</div>
               <div v-html="item.analysis"></div>
@@ -55,8 +68,9 @@
 </template>
 
 <script setup name="PostManageEvaluationView" lang="ts">
-import { getCurrentInstance, type ComponentInternalInstance, ref, onMounted } from 'vue';
+import { getCurrentInstance, type ComponentInternalInstance, ref, onMounted, nextTick } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
+import * as echarts from 'echarts';
 import PageShell from '@/components/PageShell/index.vue';
 import { getEvaluation, getExamScoreList, getExamPaperQuestions, getExamAnswerList } from '@/api/main/evaluation/index';
 
@@ -69,129 +83,279 @@ const evaluationId = route.query.evaluationId as string;
 const studentId = route.query.studentId as string;
 const studentName = ref((route.query.name as string) || '');
 const positionName = ref('');
+const startTime = ref('');
+const commitTime = ref('');
+const ansTime = ref('');
 
 const scores = ref<any[]>([]);
 const questionList = ref<any[]>([]);
-
-const parseJson = (str: any) => {
-  if (typeof str !== 'string') return str || {};
-  try { return JSON.parse(str); } catch (e) { return {}; }
+const radarChartRef = ref<HTMLElement | null>(null);
+let radarChart: echarts.ECharts | null = null;
+
+const parseBizContent = (res: any) => {
+  const msgStr = res?.msg;
+  if (typeof msgStr !== 'string') return {};
+  try {
+    const parsed = JSON.parse(msgStr);
+    return parsed?.bizContent || parsed || {};
+  } catch (e) {
+    return {};
+  }
 };
 
-const findStudentInList = (data: any, sid: string, sname: string) => {
-  let list = Array.isArray(data) ? data : (data?.data?.list || data?.list || data?.data || data?.rows || data?.records || []);
-  if (!Array.isArray(list)) {
-      if(data && typeof data === 'object' && Object.keys(data).length > 0) {
-          if (sid && (String(data.userId) === String(sid) || String(data.user_id) === String(sid))) {
-              return data;
-          }
-      }
-      return null;
+const findStudentInScoreList = (bizContent: any, sid: string) => {
+  const rows: any[] = bizContent?.rows || [];
+  if (!Array.isArray(rows) || !sid) return null;
+  let result: any = null;
+  for (const item of rows) {
+    if (String(item.userId) === String(sid)) {
+      result = item;
+    }
   }
-  return list.find((item: any) => 
-    (sid && (String(item.userId) === String(sid) || String(item.user_id) === String(sid) || String(item.studentId) === String(sid))) || 
-    (sname && (String(item.userName) === String(sname) || String(item.user_name) === String(sname) || String(item.name) === String(sname)))
-  );
+  return result;
 };
 
-const extractQuestions = (data: any) => {
-  let qlist = Array.isArray(data) ? data : (data?.data?.questions || data?.data?.questionList || data?.questions || data?.questionList || data?.data || []);
-  return Array.isArray(qlist) ? qlist : [];
+const convertComAnsToLabel = (comAns: string): string => {
+  if (!comAns) return '';
+  const keyMap: Record<string, string> = { key1: 'A', key2: 'B', key3: 'C', key4: 'D' };
+  const keys = comAns.split(',').map((k: string) => k.trim()).filter(Boolean);
+  return keys.map((k: string) => keyMap[k] || k).join('');
 };
 
-const extractAnswers = (ansData: any) => {
-    let answers = ansData?.details || ansData?.answerList || ansData?.answers || ansData?.questionList || ansData?.data || [];
-    return Array.isArray(answers) ? answers : [];
+const TYPE_NAME_MAP: Record<number, string> = {
+  1: '单选题',
+  2: '多选题',
+  3: '判断题',
+  5: '问答题'
 };
 
-const combineQA = (questions: any[], answers: any[]) => {
+const extractQuestions = (bizContent: any, ansMap: Map<string, any>) => {
+  const rows: any[] = bizContent?.rows || [];
+  if (!Array.isArray(rows)) return [];
+
+  const questionToTestId = new Map<string, string>();
+  ansMap.forEach((detail: any, testId: string) => {
+    const q = detail.question || '';
+    if (q) {
+      questionToTestId.set(q, testId);
+    }
+  });
+
+  const questions: any[] = [];
+  let seq = 0;
+  rows.forEach((row: any) => {
+    const tc = row.testContent || {};
+    const qType = Number(tc.type);
+    const questionTitle = tc.question || '';
+    const analysis = tc.analysis || '';
+
+    let questionId = questionToTestId.get(questionTitle);
+    if (!questionId) {
+      questionId = tc.test_id || row.testId || `q_${seq}`;
+    }
+
+    const options: { label: string; text: string; correct: boolean }[] = [];
+    if (qType === 1 || qType === 2) {
+      const answerFields = ['answer1', 'answer2', 'answer3', 'answer4'];
+      const keyFields = ['key1', 'key2', 'key3', 'key4'];
+      const labels = ['A', 'B', 'C', 'D'];
+      for (let i = 0; i < 4; i++) {
+        const text = tc[answerFields[i]];
+        if (text) {
+          options.push({
+            label: labels[i],
+            text,
+            correct: tc[keyFields[i]] === '1'
+          });
+        }
+      }
+    }
+
+    questions.push({
+      id: questionId,
+      questionTitle,
+      analysis,
+      options,
+      type: qType,
+      typeName: TYPE_NAME_MAP[qType] || '其他',
+      testName: row.testName || ''
+    });
+    seq++;
+  });
+  return questions;
+};
+
+const extractAnswers = (bizContent: any, sid: string) => {
+  const rows: any[] = bizContent?.rows || [];
+  if (!Array.isArray(rows) || !sid) return new Map<string, any>();
+  const studentRow = rows.find((item: any) => String(item.userId) === String(sid));
+  if (!studentRow?.ansAndScore) return new Map<string, any>();
+
+  const ansMap = new Map<string, any>();
+  const ansAndScore: any[] = studentRow.ansAndScore;
+  for (const group of ansAndScore) {
+    if (!Array.isArray(group)) continue;
+    for (let i = 1; i < group.length; i++) {
+      const entry = group[i];
+      if (Array.isArray(entry) && entry.length >= 2) {
+        const testId = String(entry[0]);
+        const detail = entry[1] || {};
+        ansMap.set(testId, detail);
+      }
+    }
+  }
+  return ansMap;
+};
+
+const combineQA = (questions: any[], ansMap: Map<string, any>) => {
   return questions.map((q, index) => {
-    let ans = answers.find(a => String(a.questionId) === String(q.id) || String(a.question_id) === String(q.id) || String(a.id) === String(q.id));
-    
-    let optionsObj = q.options || q.qOption || q.questionOptions || [];
-    if (typeof optionsObj === 'string') {
-      try { optionsObj = JSON.parse(optionsObj); } catch(e) {}
+    const ans = ansMap.get(String(q.id));
+
+    let correct = false;
+    if (ans) {
+      if (ans.is_ok === 'right') correct = true;
     }
-    
-    let formattedOptions = Array.isArray(optionsObj) ? optionsObj.map((o: any, idx: number) => {
-        return {
-            label: o.label || o.key || o.option || String.fromCharCode(65 + idx),
-            text: o.text || o.content || o.value || o.optionDesc || '',
-            correct: !!o.correct || !!o.isRight || !!o.isCorrect
-        };
-    }) : [];
-
-    let c = false;
+
+    let answer = '';
     if (ans) {
-        if(ans.isCorrect !== undefined) c = !!ans.isCorrect;
-        else if (ans.correct !== undefined) c = !!ans.correct;
-        else if (Number(ans.score) > 0) c = true;
+      const comAns = ans.com_ans || '';
+      if (comAns && (q.type === 1 || q.type === 2)) {
+        answer = convertComAnsToLabel(comAns);
+      } else if (comAns) {
+        answer = comAns;
+      }
     }
 
+    const testAnsRight = ans?.test_ans_right || '';
+    const options = q.options.map((o: any) => ({ ...o }));
+
     return {
-       id: q.id || `q_${index}_${Math.random().toString(36).substring(2, 6)}`,
-       questionTitle: q.title || q.content || q.questionContent || '未命名题目',
-       correct: c,
-       score: ans?.score || 0,
-       answer: ans?.userAnswer || ans?.answer || ans?.myAnswer || '',
-       options: formattedOptions.length > 0 ? formattedOptions : [ { label: 'A', text: '选项A' }, { label: 'B', text: '选项B' } ],
-       analysis: q.analysis || q.answerAnalysis || q.qAnalysis || q.questionAnalysis || ''
+      id: q.id || `q_${index}`,
+      questionTitle: q.questionTitle || '未命名题目',
+      correct,
+      score: ans?.score ?? 0,
+      answer,
+      options: options.length > 0 ? options : q.type === 5 ? [] : [{ label: 'A', text: '选项A' }, { label: 'B', text: '选项B' }],
+      analysis: q.analysis || '',
+      typeName: q.typeName,
+      testName: q.testName,
+      testAnsRight
     };
   });
 };
 
-const loadData = async () => {
-    if(!evaluationId) return;
-    loading.value = true;
-    try {
-        const res = await getEvaluation(evaluationId);
-        const evalData = res.data;
-        positionName.value = evalData.position || '未知岗位';
-
-        const abilityConfigs = evalData.abilityConfigs || [];
-        let allQuestions: any[] = [];
-        let scoreItems: any[] = [];
-
-        for (const ability of abilityConfigs) {
-            const examId = ability.thirdExamInfoId;
-            if(!examId) continue;
-
-            const scoreRes = await getExamScoreList({ examInfoId: examId, page: 1 }).catch(()=>({data: {}}));
-            const scoreData = parseJson(scoreRes.data);
-            const myScoreObj = findStudentInList(scoreData, studentId, studentName.value);
-            
-            const score = myScoreObj?.totalScore || myScoreObj?.score || myScoreObj?.examScore || 0;
-            const passMark = ability.thirdExamPassMark || 0;
-            const pass = Number(score) >= Number(passMark);
-
-            scoreItems.push({
-                abilityName: ability.abilityName || ability.thirdExamName || '考核',
-                score: score,
-                totalScore: ability.thirdExamTotalScore || 100,
-                pass
-            });
-
-            const paperRes = await getExamPaperQuestions({ examInfoId: examId }).catch(()=>({data: {}}));
-            const paperData = parseJson(paperRes.data);
-            const questions = extractQuestions(paperData);
-
-            const answerRes = await getExamAnswerList({ examInfoId: examId }).catch(()=>({data: {}}));
-            const answerData = parseJson(answerRes.data);
-            const myAnswerObj = findStudentInList(answerData, studentId, studentName.value);
-            const answers = myAnswerObj ? extractAnswers(myAnswerObj) : [];
-
-            allQuestions.push(...combineQA(questions, answers));
+const renderRadarChart = () => {
+  if (!radarChartRef.value || scores.value.length < 3) return;
+
+  if (!radarChart) {
+    radarChart = echarts.init(radarChartRef.value);
+  }
+
+  const indicator = scores.value.map((s: any) => ({
+    name: s.abilityName,
+    max: Number(s.totalScore) || 100
+  }));
+
+  const dataValues = scores.value.map((s: any) => Number(s.score) || 0);
+
+  radarChart.setOption({
+    tooltip: {
+      trigger: 'item'
+    },
+    radar: {
+      indicator,
+      radius: '65%',
+      name: {
+        textStyle: {
+          fontSize: 11
+        }
+      },
+      splitArea: {
+        areaStyle: {
+          color: ['rgba(64,158,255,0.05)', 'rgba(64,158,255,0.1)', 'rgba(64,158,255,0.15)', 'rgba(64,158,255,0.2)']
+        }
+      }
+    },
+    series: [{
+      type: 'radar',
+      data: [{
+        value: dataValues,
+        name: '得分',
+        areaStyle: {
+          color: 'rgba(64,158,255,0.2)'
+        },
+        lineStyle: {
+          color: '#409EFF'
+        },
+        itemStyle: {
+          color: '#409EFF'
         }
+      }]
+    }]
+  });
+};
+
+const loadData = async () => {
+  if (!evaluationId) return;
+  loading.value = true;
+  try {
+    const res = await getEvaluation(evaluationId);
+    const evalData = res.data;
+    positionName.value = evalData.position || '未知岗位';
+
+    const abilityConfigs = evalData.abilityConfigs || [];
+    let allQuestions: any[] = [];
+    let scoreItems: any[] = [];
+
+    for (const ability of abilityConfigs) {
+      const examId = ability.thirdExamInfoId;
+      if (!examId) continue;
+
+      const scoreRes = await getExamScoreList({ examInfoId: examId, page: 1 }).catch(() => ({ msg: '{}' }));
+      const scoreBiz = parseBizContent(scoreRes);
+      const myScoreObj = findStudentInScoreList(scoreBiz, studentId);
+
+      const score = myScoreObj?.score || 0;
+      const passMark = ability.thirdExamPassMark || ability.passingScore || 0;
+      const pass = myScoreObj ? String(myScoreObj.isPass) === '1' : Number(score) >= Number(passMark);
+
+      // 取考生时间信息
+      if (myScoreObj && !startTime.value) {
+        startTime.value = myScoreObj.startTime || '';
+        commitTime.value = myScoreObj.commitTime || '';
+        ansTime.value = myScoreObj.ansTime || '';
+      }
+
+      scoreItems.push({
+        abilityName: ability.abilityName || ability.thirdExamName || '考核',
+        score: score,
+        totalScore: ability.thirdExamTotalScore || ability.score || 100,
+        pass
+      });
 
-        scores.value = scoreItems;
-        questionList.value = allQuestions;
+      const answerRes = await getExamAnswerList({ examInfoId: examId }).catch(() => ({ msg: '{}' }));
+      const answerBiz = parseBizContent(answerRes);
+      const ansMap = extractAnswers(answerBiz, studentId);
 
-    } catch (e) {
-        console.error(e);
-        proxy?.$modal?.msgError('获取测评数据失败');
-    } finally {
-        loading.value = false;
+      const paperRes = await getExamPaperQuestions({ examInfoId: examId }).catch(() => ({ msg: '{}' }));
+      const paperBiz = parseBizContent(paperRes);
+      const questions = extractQuestions(paperBiz, ansMap);
+
+      allQuestions.push(...combineQA(questions, ansMap));
     }
+
+    scores.value = scoreItems;
+    questionList.value = allQuestions;
+
+    await nextTick();
+    renderRadarChart();
+
+  } catch (e) {
+    console.error(e);
+    proxy?.$modal?.msgError('获取测评数据失败');
+  } finally {
+    loading.value = false;
+  }
 };
 
 const handleBack = () => {
@@ -200,22 +364,31 @@ const handleBack = () => {
 };
 
 onMounted(() => {
-    loadData();
+  loadData();
 });
 </script>
 
 <style scoped>
 .evaluation-view-page {
   display: grid;
-  grid-template-columns: 180px 1fr;
+  grid-template-columns: 280px 1fr;
   gap: 16px;
 }
 
-.left-card,
+.left-card {
+  background: #fff;
+  border-radius: 6px;
+  padding: 16px;
+  height: calc(100vh - 180px);
+  overflow-y: auto;
+}
+
 .right-card {
   background: #fff;
   border-radius: 6px;
   padding: 16px;
+  height: calc(100vh - 180px);
+  overflow-y: auto;
 }
 
 .card-title {
@@ -252,6 +425,12 @@ onMounted(() => {
   align-items: center;
   justify-content: center;
   color: #909399;
+  font-size: 12px;
+}
+
+.radar-chart {
+  height: 220px;
+  margin-top: 8px;
 }
 
 .footer-btn {
@@ -298,6 +477,12 @@ onMounted(() => {
   display: flex;
   align-items: center;
   gap: 8px;
+  padding: 4px 8px;
+  border-radius: 4px;
+}
+
+.question-option.is-user-answer {
+  background: #ecf5ff;
 }
 
 .correct-text {
@@ -310,9 +495,22 @@ onMounted(() => {
   color: #606266;
   font-size: 13px;
 }
-</style>
-  margin-top: 16px;
+
+.question-user-answer {
+  margin-top: 8px;
+  padding: 8px 12px;
+  background: #f5f7fa;
+  border-radius: 4px;
   color: #606266;
   font-size: 13px;
 }
+
+.question-right-answer {
+  margin-top: 8px;
+  padding: 8px 12px;
+  background: #f0f9eb;
+  border-radius: 4px;
+  color: #67c23a;
+  font-size: 13px;
+}
 </style>

+ 37 - 5
src/views/postManage/index.vue

@@ -135,7 +135,7 @@
           <span class="import-notice-text">首次导入先下载导入模板。</span>
           <el-link type="primary" :underline="false" class="import-notice-link" @click="handleDownloadImportTemplate">立即下载</el-link>
         </div>
-        <el-upload :limit="1" accept=".xlsx,.xls" :auto-upload="false" drag>
+        <el-upload ref="uploadRef" :limit="1" accept=".xlsx,.xls" :headers="importDialog.headers" :action="importDialog.url" :disabled="importDialog.isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag>
           <el-icon class="el-icon--upload">
             <i-ep-upload-filled />
           </el-icon>
@@ -163,10 +163,12 @@
 <script setup name="Post" lang="ts">
 import { getCurrentInstance, onMounted, reactive, ref, type ComponentInternalInstance } from 'vue';
 import { useRouter } from 'vue-router';
+import { ElMessageBox } from 'element-plus';
 import { getMainAudit, listMainAudit } from '@/api/main/audit';
 import PageShell from '@/components/PageShell/index.vue';
 import { delPostManage, listPostManage, publishPostManage, unpublishPostManage } from '@/api/main/postManage';
 import { MainPostApplyQuery, MainPostApplyVO } from '@/api/main/postManage/types';
+import { globalHeaders } from '@/utils/request';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const router = useRouter();
@@ -195,10 +197,20 @@ const auditStatusOptions = [
   { label: '驳回', value: 3 }
 ];
 
-const importDialog = reactive<DialogOption>({
+const importDialog = reactive<{
+  visible: boolean;
+  title: string;
+  isUploading: boolean;
+  headers: Record<string, string>;
+  url: string;
+}>({
   visible: false,
-  title: '批量导入'
+  title: '批量导入',
+  isUploading: false,
+  headers: globalHeaders(),
+  url: import.meta.env.VITE_APP_BASE_API + '/main/postApply/importData'
 });
+const uploadRef = ref<any>(null);
 
 const auditRemarkDialog = reactive<{ visible: boolean; data: PostAuditRemarkVO }>({
   visible: false,
@@ -366,15 +378,35 @@ const handleDelete = async (row?: MainPostApplyVO) => {
 };
 
 const handleImportOpen = () => {
+  importDialog.headers = globalHeaders();
   importDialog.visible = true;
 };
 
-const submitImportForm = () => {
+/** 文件上传中处理 */
+const handleFileUploadProgress = () => {
+  importDialog.isUploading = true;
+};
+
+/** 文件上传成功处理 */
+const handleFileSuccess = (response: any, file: any) => {
+  importDialog.isUploading = false;
   importDialog.visible = false;
+  uploadRef.value?.handleRemove(file);
+  ElMessageBox.alert(
+    "<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + '</div>',
+    '导入结果',
+    { dangerouslyUseHTMLString: true }
+  );
+  getList();
+};
+
+/** 提交上传文件 */
+const submitImportForm = () => {
+  uploadRef.value?.submit();
 };
 
 const handleDownloadImportTemplate = () => {
-  modal?.msgSuccess('待接入模板下载');
+  proxy?.download('main/postApply/importTemplate', {}, `post_template_${new Date().getTime()}.xlsx`);
 };
 
 onMounted(() => {