Gqingci преди 10 часа
родител
ревизия
3298eab04f

+ 58 - 0
src/api/main/dashboard/index.ts

@@ -0,0 +1,58 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+
+// 岗位管理近日发布数量报表数据项
+export interface TrendData {
+  date: string;
+  count: number;
+}
+
+/**
+ * 获取岗位管理近日发布数量报表
+ */
+export function getPostRecentStats(): AxiosPromise<TrendData[]> {
+  return request({
+    url: '/main/dashboard/postRecentStats',
+    method: 'get'
+  });
+}
+
+/**
+ * 获取测评管理总发布数量报表
+ */
+export function getEvaluationTotalStats(): AxiosPromise<number> {
+  return request({
+    url: '/main/dashboard/evaluationTotalStats',
+    method: 'get'
+  });
+}
+
+/**
+ * 获取当前租户/公司的员工人数
+ */
+export function getCompanyStats(): AxiosPromise<number> {
+  return request({
+    url: '/main/dashboard/companyStats',
+    method: 'get'
+  });
+}
+
+/**
+ * 获取当前租户的部门数量
+ */
+export function getDeptStats(): AxiosPromise<number> {
+  return request({
+    url: '/main/dashboard/deptStats',
+    method: 'get'
+  });
+}
+
+/**
+ * 获取昨日加入公司人数
+ */
+export function getUserYesterdayStats(): AxiosPromise<number> {
+  return request({
+    url: '/main/dashboard/userYesterdayStats',
+    method: 'get'
+  });
+}

+ 24 - 0
src/api/main/evaluation/index.ts

@@ -61,3 +61,27 @@ export function removeEvaluationApply(applyId: string | number) {
     method: 'delete'
   });
 }
+
+export function getExamScoreList(data: { examInfoId: string | number; page?: number }) {
+  return request({
+    url: '/main/examEvaluation/score-list',
+    method: 'post',
+    data
+  });
+}
+
+export function getExamPaperQuestions(data: { examInfoId: string | number }) {
+  return request({
+    url: '/main/examEvaluation/paper-questions',
+    method: 'post',
+    data
+  });
+}
+
+export function getExamAnswerList(data: { examInfoId: string | number }) {
+  return request({
+    url: '/main/examEvaluation/answer-list',
+    method: 'post',
+    data
+  });
+}

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

@@ -22,6 +22,12 @@ export interface PostCandidateVO {
   remark?: string;
 }
 
+export interface PostCandidateHireForm {
+  id: string | number;
+  remark?: string;
+  offerAttachment: string;
+}
+
 export interface PostCandidateStatusForm {
   id: string | number;
   status: string;
@@ -43,3 +49,11 @@ export function updatePostCandidateStatus(data: PostCandidateStatusForm) {
     data
   });
 }
+
+export function hirePostCandidate(data: PostCandidateHireForm) {
+  return request({
+    url: '/main/postCandidate/hire',
+    method: 'post',
+    data
+  });
+}

+ 125 - 0
src/api/main/student.ts

@@ -0,0 +1,125 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+
+export interface MainStudentEducationVO {
+  id?: string | number;
+  studentId?: string | number;
+  school?: string;
+  education?: string;
+  startTime?: string;
+  endTime?: string;
+  major?: string;
+  campusExperience?: string;
+}
+
+export interface MainStudentExperienceVO {
+  id?: string | number;
+  studentId?: string | number;
+  company?: string;
+  industry?: string;
+  startTime?: string;
+  endTime?: string;
+  isInternship?: number;
+  jobTitle?: string;
+  department?: string;
+  workContent?: string;
+}
+
+export interface MainStudentProjectVO {
+  id?: string | number;
+  studentId?: string | number;
+  projectName?: string;
+  role?: string;
+  startTime?: string;
+  endTime?: string;
+  description?: string;
+  achievement?: string;
+  link?: string;
+}
+
+export interface MainExamApplyRecordVO {
+  id?: string | number;
+  evaluationId?: string | number;
+  evaluationName?: string;
+  positionName?: string;
+  studentId?: string | number;
+  applyStatus?: string;
+  finalResult?: string;
+  statusText?: string;
+  statusType?: string;
+  scheduleStartTime?: string;
+  deadlineTime?: string;
+  finishedTime?: string;
+  createTime?: string;
+}
+
+export interface StudentTrainingRecordVO {
+  id?: string | number;
+  trainingId?: string | number;
+  studentId?: string | number;
+  name?: string;
+  trainingType?: string;
+  trainingStartTime?: string;
+  trainingEndTime?: string;
+  progress?: number;
+  learnStatus?: number;
+  enrollStatus?: number;
+  checkInTime?: string;
+}
+
+export interface StudentJobRecordVO {
+  id?: string | number;
+  candidateId?: string | number;
+  tenantId?: string;
+  postId?: string | number;
+  studentId?: string | number;
+  employmentStatus?: string;
+  entryTime?: string;
+  leaveTime?: string;
+  leaveReason?: string;
+  companyName?: string;
+  postName?: string;
+  postType?: string;
+}
+
+export interface MainStudentVO {
+  id?: string | number;
+  studentNo?: string;
+  name?: string;
+  mobile?: string;
+  email?: string;
+  idCardNumber?: string;
+  gender?: string;
+  avatar?: string | number;
+  avatarUrl?: string;
+  userType?: string;
+  userTypeLabel?: string;
+  totalAmount?: number;
+  availability?: string;
+  jobIntention?: string;
+  intentionCompanies?: string;
+  jobType?: string;
+  schoolName?: string;
+  education?: string;
+  grade?: string;
+  internshipDuration?: string;
+  resumeFile?: string | number;
+  status?: string;
+  loginDate?: string;
+  createTime?: string;
+  updateTime?: string;
+  remark?: string;
+  educationList?: MainStudentEducationVO[];
+  experienceList?: MainStudentExperienceVO[];
+  projectList?: MainStudentProjectVO[];
+  evaluationList?: MainExamApplyRecordVO[];
+  trainingList?: StudentTrainingRecordVO[];
+  jobList?: StudentJobRecordVO[];
+}
+
+export function getStudentInfo(id: string | number): AxiosPromise<MainStudentVO> {
+  return request({
+    url: `/main/student/${id}`,
+    method: 'get'
+  });
+}

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

@@ -169,7 +169,18 @@ const handleAddEmployee = () => {
 };
 
 const handleExport = () => {
-  modal?.msgSuccess('导出列表功能待接入');
+  if (!evaluationId.value) {
+    modal?.msgWarning('测评ID不能为空');
+    return;
+  }
+  proxy?.download(
+    '/main/examEvaluation/applyList/export',
+    {
+      evaluationId: evaluationId.value,
+      keyword: queryParams.keyword.trim()
+    },
+    `evaluation_apply_${new Date().getTime()}.xlsx`
+  );
 };
 
 const handleConfirmSync = async () => {
@@ -202,6 +213,7 @@ const handleOpenDetail = (row: EvaluationApplyRow) => {
     path: '/evaluation/evaluation-view',
     query: {
       applyId: String(row.id),
+      studentId: String(row.studentId || ''),
       name: row.name || '',
       evaluationId: String(route.query.evaluationId || '')
     }

+ 7 - 2
src/views/evaluation/components/EvaluationTable.vue

@@ -43,9 +43,14 @@
         />
       </template>
     </el-table-column>
-    <el-table-column label="同步时间" min-width="170" align="center">
+    <el-table-column label="上架时间" min-width="120" align="center">
       <template #default="{ row }">
-        <span>{{ formatDateTime(row.updateTime) }}</span>
+        <span>{{ formatDateTime(row.onTime) }}</span>
+      </template>
+    </el-table-column>
+    <el-table-column label="下架时间" min-width="120" align="center">
+      <template #default="{ row }">
+        <span>{{ formatDateTime(row.downTime) }}</span>
       </template>
     </el-table-column>
     <el-table-column label="状态" min-width="120" align="center">

+ 166 - 74
src/views/evaluation/components/evaluation-view.vue

@@ -1,115 +1,207 @@
 <template>
   <PageShell>
-    <div class="evaluation-view-page">
+    <div class="evaluation-view-page" v-loading="loading">
       <div class="left-card">
         <div class="card-title">考生信息</div>
         <div class="info-list">
-          <div class="info-item"><span>姓名:</span><span>张三</span></div>
-          <div class="info-item"><span>报名岗位:</span><span>审计</span></div>
-          <div class="info-item"><span>开始时间:</span><span>2024-06-13 09:49:47</span></div>
-          <div class="info-item"><span>结束时间:</span><span>2024-06-13 09:50:26</span></div>
-          <div class="info-item"><span>答题时长:</span><span>0分39秒</span></div>
-          <div class="info-item"><span>浏览器:</span><span>unknown Browser</span></div>
-          <div class="info-item"><span>终端设备:</span><span>iPhone iOS 17.5.1</span></div>
-          <div class="info-item"><span>用户IP:</span><span>59.46.39.185</span></div>
+          <div class="info-item"><span>姓名:</span><span>{{ studentName }}</span></div>
+          <div class="info-item"><span>报名岗位:</span><span>{{ positionName }}</span></div>
         </div>
 
         <div class="card-title second">答题信息</div>
         <div class="answer-list">
-          <div class="answer-item"><span>能力测试:10/30</span><el-tag type="success">通过</el-tag></div>
-          <div class="answer-item"><span>实操能力:20/40</span><el-tag type="success">通过</el-tag></div>
-          <div class="answer-item"><span>性格:12/34</span><el-tag type="danger">未通过</el-tag></div>
+          <div v-for="ab in scores" :key="ab.abilityName" class="answer-item">
+            <span>{{ ab.abilityName }}:{{ ab.score }}/{{ ab.totalScore }}</span>
+            <el-tag :type="ab.pass ? 'success' : 'danger'">{{ ab.pass ? '通过' : '未通过' }}</el-tag>
+          </div>
+          <div v-if="!scores.length" class="answer-item" style="color:#999">暂无答题记录</div>
         </div>
 
         <div class="card-title second">维度分析</div>
-        <div class="chart-placeholder">维度图</div>
+        <div class="chart-placeholder">暂不支持</div>
         <div class="footer-btn">
           <el-button @click="handleBack">返回</el-button>
-          <el-button type="primary">下一份</el-button>
         </div>
       </div>
 
       <div class="right-card">
-        <div v-for="item in questionList" :key="item.id" class="question-block">
-          <div class="question-head">
-            <span>第{{ item.id }}题</span>
-            <div class="question-result">
-              <el-tag :type="item.correct ? 'success' : 'danger'">{{ item.correct ? '正确' : '错误' }}</el-tag>
-              <span>得分</span>
-              <el-input :model-value="String(item.score)" class="score-input" readonly />
+        <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>
+              <div class="question-result">
+                <el-tag :type="item.correct ? 'success' : 'danger'">{{ item.correct ? '正确' : '错误' }}</el-tag>
+                <span>得分</span>
+                <el-input :model-value="String(item.score)" class="score-input" readonly />
+              </div>
             </div>
-          </div>
-          <div class="question-title">请选择一个选项</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 }}.{{ option.text }}</el-radio>
-              <span v-if="option.correct" class="correct-text">正确答案</span>
+            <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>
+            </div>
+            <div v-if="item.analysis" class="question-analysis">
+              <div>答案解析:</div>
+              <div v-html="item.analysis"></div>
             </div>
           </div>
-          <div v-if="item.analysis" class="question-analysis">
-            <div>答案解析:</div>
-            <div>{{ item.analysis }}</div>
-          </div>
-        </div>
+        </template>
+        <el-empty v-else description="暂无答题详情数据" />
       </div>
     </div>
   </PageShell>
 </template>
 
 <script setup name="PostManageEvaluationView" lang="ts">
-import { getCurrentInstance, type ComponentInternalInstance } from 'vue';
-import { useRouter } from 'vue-router';
+import { getCurrentInstance, type ComponentInternalInstance, ref, onMounted } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
 import PageShell from '@/components/PageShell/index.vue';
+import { getEvaluation, getExamScoreList, getExamPaperQuestions, getExamAnswerList } from '@/api/main/evaluation/index';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const router = useRouter();
+const route = useRoute();
+
+const loading = ref(false);
+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 questionList = [
-  {
-    id: 1,
-    correct: true,
-    score: 5,
-    answer: 'A',
-    options: [
-      { label: 'A', text: '选项1', correct: true },
-      { label: 'B', text: '选项2', correct: false },
-      { label: 'C', text: '选项3', correct: false },
-      { label: 'D', text: '选项4', correct: false }
-    ],
-    analysis: ''
-  },
-  {
-    id: 2,
-    correct: true,
-    score: 5,
-    answer: 'A',
-    options: [
-      { label: 'A', text: '选项1', correct: true },
-      { label: 'B', text: '选项2', correct: false },
-      { label: 'C', text: '选项3', correct: false },
-      { label: 'D', text: '选项4', correct: false }
-    ],
-    analysis: ''
-  },
-  {
-    id: 3,
-    correct: false,
-    score: 0,
-    answer: 'C',
-    options: [
-      { label: 'A', text: '选项1', correct: true },
-      { label: 'B', text: '选项2', correct: false },
-      { label: 'C', text: '选项3', correct: false },
-      { label: 'D', text: '选项4', correct: false }
-    ],
-    analysis: '解析解析解析解析解析解析解析解析'
+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 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;
   }
-];
+  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)))
+  );
+};
+
+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 extractAnswers = (ansData: any) => {
+    let answers = ansData?.details || ansData?.answerList || ansData?.answers || ansData?.questionList || ansData?.data || [];
+    return Array.isArray(answers) ? answers : [];
+};
+
+const combineQA = (questions: any[], answers: 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) {}
+    }
+    
+    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;
+    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;
+    }
+
+    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 || ''
+    };
+  });
+};
+
+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));
+        }
+
+        scores.value = scoreItems;
+        questionList.value = allQuestions;
+
+    } catch (e) {
+        console.error(e);
+        proxy?.$modal?.msgError('获取测评数据失败');
+    } finally {
+        loading.value = false;
+    }
+};
 
 const handleBack = () => {
   proxy?.$tab.closePage();
   router.back();
 };
+
+onMounted(() => {
+    loadData();
+});
 </script>
 
 <style scoped>

+ 11 - 3
src/views/evaluation/index.vue

@@ -108,8 +108,10 @@ const queryParams = reactive<MainExamEvaluationQuery>({
 
 const normalizedQuery = computed(() => ({
   ...queryParams,
-  beginTime: dateRange.value?.[0] || '',
-  endTime: dateRange.value?.[1] || ''
+  params: {
+    beginTime: dateRange.value?.[0] || undefined,
+    endTime: dateRange.value?.[1] || undefined
+  }
 }));
 
 const mapDictOptions = (list: DictDataVO[] = []): FilterOption[] =>
@@ -229,7 +231,13 @@ const handleViewApplyList = (row: MainExamEvaluationVO) => {
 };
 
 const handleExport = () => {
-  modal?.msgSuccess('导出数据功能待接入');
+  proxy?.download(
+    '/main/examEvaluation/export',
+    {
+      ...normalizedQuery.value
+    },
+    `evaluation_${new Date().getTime()}.xlsx`
+  );
 };
 
 const handleOpenSyncDialog = () => {

+ 301 - 138
src/views/index.vue

@@ -1,164 +1,327 @@
 <template>
-  <div class="app-container home">
-    <el-row :gutter="20">
-      <el-col :sm="24" :lg="12" style="padding-left: 20px">
-        <h2>RuoYi-Vue-Plus多租户管理系统</h2>
-        <p>
-          RuoYi-Vue-Plus 是基于 RuoYi-Vue 针对 分布式集群 场景升级(不兼容原框架)
-          <br />
-          * 前端开发框架 Vue3、TS、Element Plus<br />
-          * 后端开发框架 Spring Boot<br />
-          * 容器框架 Undertow 基于 Netty 的高性能容器<br />
-          * 权限认证框架 Sa-Token 支持多终端认证系统<br />
-          * 关系数据库 MySQL 适配 8.X 最低 5.7<br />
-          * 缓存数据库 Redis 适配 6.X 最低 4.X<br />
-          * 数据库框架 Mybatis-Plus 快速 CRUD 增加开发效率<br />
-          * 数据库框架 p6spy 更强劲的 SQL 分析<br />
-          * 多数据源框架 dynamic-datasource 支持主从与多种类数据库异构<br />
-          * 序列化框架 Jackson 统一使用 jackson 高效可靠<br />
-          * Redis客户端 Redisson 性能强劲、API丰富<br />
-          * 分布式限流 Redisson 全局、请求IP、集群ID 多种限流<br />
-          * 分布式锁 Lock4j 注解锁、工具锁 多种多样<br />
-          * 分布式幂等 Lock4j 基于分布式锁实现<br />
-          * 分布式链路追踪 SkyWalking 支持链路追踪、网格分析、度量聚合、可视化<br />
-          * 分布式任务调度 SnailJob 高性能 高可靠 易扩展<br />
-          * 文件存储 Minio 本地存储<br />
-          * 文件存储 七牛、阿里、腾讯 云存储<br />
-          * 监控框架 SpringBoot-Admin 全方位服务监控<br />
-          * 校验框架 Validation 增强接口安全性 严谨性<br />
-          * Excel框架 FastExcel(原Alibaba EasyExcel) 性能优异 扩展性强<br />
-          * 文档框架 SpringDoc、javadoc 无注解零入侵基于java注释<br />
-          * 工具类框架 Hutool、Lombok 减少代码冗余 增加安全性<br />
-          * 代码生成器 适配MP、SpringDoc规范化代码 一键生成前后端代码<br />
-          * 部署方式 Docker 容器编排 一键部署业务集群<br />
-          * 国际化 SpringMessage Spring标准国际化方案<br />
-        </p>
-        <p><b>当前版本:</b> <span>v5.5.3</span></p>
-        <p>
-          <el-tag type="danger">&yen;免费开源</el-tag>
-        </p>
-        <p>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://gitee.com/dromara/RuoYi-Vue-Plus')">访问码云</el-button>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://github.com/dromara/RuoYi-Vue-Plus')">访问GitHub</el-button>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-vue-plus/changlog')"
-            >更新日志</el-button
-          >
-        </p>
+  <div class="app-container dashboard-wrapper">
+    <!-- Header / Welcome Section -->
+    <el-card class="welcome-card" shadow="hover">
+      <div class="welcome-content">
+        <div class="welcome-text">
+          <h2 class="greeting">欢迎回来,商户管理员!👋</h2>
+          <p class="subtitle">这里是您的综合业务数据概览,实时获取最新的运营数据。</p>
+        </div>
+        <div class="welcome-image">
+          <el-icon class="welcome-icon"><DataBoard /></el-icon>
+        </div>
+      </div>
+    </el-card>
+
+    <!-- Data Summary Cards -->
+    <el-row :gutter="20" class="stat-cards" v-loading="loadingStats">
+      <el-col :xs="24" :sm="12" :md="6">
+        <el-card shadow="hover" :body-style="{ padding: '30px' }" class="stat-card">
+          <div class="stat-header">
+            <span class="stat-title">测评管理总发布数量</span>
+            <el-tag type="success" effect="light" size="small">总数</el-tag>
+          </div>
+          <div class="stat-body">
+            <span class="stat-value">{{ evaluationTotalCount }}</span>
+            <el-icon class="stat-icon" style="color: #67c23a"><Document /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :xs="24" :sm="12" :md="6">
+        <el-card shadow="hover" :body-style="{ padding: '30px' }" class="stat-card">
+          <div class="stat-header">
+            <span class="stat-title">部门数量</span>
+            <el-tag type="warning" effect="light" size="small">部门</el-tag>
+          </div>
+          <div class="stat-body">
+            <span class="stat-value">{{ deptCount }}</span>
+            <el-icon class="stat-icon" style="color: #e6a23c"><OfficeBuilding /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :xs="24" :sm="12" :md="6">
+        <el-card shadow="hover" :body-style="{ padding: '30px' }" class="stat-card">
+          <div class="stat-header">
+            <span class="stat-title">昨日加入公司人数</span>
+            <el-tag type="danger" effect="light" size="small">昨日</el-tag>
+          </div>
+          <div class="stat-body">
+            <span class="stat-value">{{ userYesterdayCount }}</span>
+            <el-icon class="stat-icon" style="color: #f56c6c"><UserFilled /></el-icon>
+          </div>
+        </el-card>
       </el-col>
+      <el-col :xs="24" :sm="12" :md="6">
+        <el-card shadow="hover" :body-style="{ padding: '30px' }" class="stat-card">
+          <div class="stat-header">
+            <span class="stat-title">公司人员总数</span>
+            <el-tag type="primary" effect="light" size="small">总共</el-tag>
+          </div>
+          <div class="stat-body">
+            <span class="stat-value">{{ companyUserCount }}</span>
+            <el-icon class="stat-icon" style="color: #409eff"><User /></el-icon>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
 
-      <el-col :sm="24" :lg="12" style="padding-left: 20px">
-        <h2>RuoYi-Cloud-Plus多租户微服务管理系统</h2>
-        <p>
-          RuoYi-Cloud-Plus 微服务通用权限管理系统 重写 RuoYi-Cloud 全方位升级(不兼容原框架)
-          <br />
-          * 前端开发框架 Vue3、TS、Element UI<br />
-          * 后端开发框架 Spring Boot<br />
-          * 微服务开发框架 Spring Cloud、Spring Cloud Alibaba<br />
-          * 容器框架 Undertow 基于 XNIO 的高性能容器<br />
-          * 权限认证框架 Sa-Token、Jwt 支持多终端认证系统<br />
-          * 关系数据库 MySQL 适配 8.X 最低 5.7<br />
-          * 关系数据库 Oracle 适配 11g 12c<br />
-          * 关系数据库 PostgreSQL 适配 13 14<br />
-          * 关系数据库 SQLServer 适配 2017 2019<br />
-          * 缓存数据库 Redis 适配 6.X 最低 5.X<br />
-          * 分布式注册中心 Alibaba Nacos 采用2.X 基于GRPC通信高性能<br />
-          * 分布式配置中心 Alibaba Nacos 采用2.X 基于GRPC通信高性能<br />
-          * 服务网关 Spring Cloud Gateway 响应式高性能网关<br />
-          * 负载均衡 Spring Cloud Loadbalancer 负载均衡处理<br />
-          * RPC远程调用 Apache Dubbo 原生态使用体验、高性能<br />
-          * 分布式限流熔断 Alibaba Sentinel 无侵入、高扩展<br />
-          * 分布式事务 Alibaba Seata 无侵入、高扩展 支持 四种模式<br />
-          * 分布式消息队列 Apache Kafka 高性能高速度<br />
-          * 分布式消息队列 Apache RocketMQ 高可用功能多样<br />
-          * 分布式消息队列 RabbitMQ 支持各种扩展插件功能多样性<br />
-          * 分布式搜索引擎 ElasticSearch 业界知名<br />
-          * 分布式链路追踪 Apache SkyWalking 链路追踪、网格分析、度量聚合、可视化<br />
-          * 分布式日志中心 ELK 业界成熟解决方案<br />
-          * 分布式监控 Prometheus、Grafana 全方位性能监控<br />
-          * 其余与 Vue 版本一致<br />
-        </p>
-        <p><b>当前版本:</b> <span>v2.5.3</span></p>
-        <p>
-          <el-tag type="danger">&yen;免费开源</el-tag>
-        </p>
-        <p>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://gitee.com/dromara/RuoYi-Cloud-Plus')">访问码云</el-button>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://github.com/dromara/RuoYi-Cloud-Plus')">访问GitHub</el-button>
-          <el-button type="primary" icon="Cloudy" plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-cloud-plus/changlog')"
-            >更新日志</el-button
-          >
-        </p>
+    <!-- Charts Section -->
+    <el-row :gutter="20" class="charts-row">
+      <el-col :xs="24" :lg="24">
+        <el-card shadow="hover" class="chart-card" v-loading="loadingChart">
+          <template #header>
+            <div class="card-header">
+              <span>岗位管理 - 近日发布数量报表</span>
+            </div>
+          </template>
+          <div ref="lineChartRef" class="echart-container" style="height: 400px;"></div>
+        </el-card>
       </el-col>
     </el-row>
-    <el-divider />
   </div>
 </template>
 
 <script setup name="Index" lang="ts">
-const goTarget = (url: string) => {
-  window.open(url, '__blank');
-};
-</script>
+import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
+import * as echarts from 'echarts';
+import { getPostRecentStats, getEvaluationTotalStats, getCompanyStats, getDeptStats, getUserYesterdayStats, TrendData } from '@/api/main/dashboard/index';
 
-<style lang="scss" scoped>
-.home {
-  blockquote {
-    padding: 10px 20px;
-    margin: 0 0 20px;
-    font-size: 17.5px;
-    border-left: 5px solid #eee;
-  }
-  hr {
-    margin-top: 20px;
-    margin-bottom: 20px;
-    border: 0;
-    border-top: 1px solid #eee;
-  }
-  .col-item {
-    margin-bottom: 20px;
-  }
+// 反应式状态
+const loadingStats = ref(true);
+const loadingChart = ref(true);
+const evaluationTotalCount = ref<number>(0);
+const deptCount = ref<number>(0);
+const userYesterdayCount = ref<number>(0);
+const companyUserCount = ref<number>(0);
 
-  ul {
-    padding: 0;
-    margin: 0;
+// 图表 Ref 与实例
+const lineChartRef = ref<HTMLElement | null>(null);
+let lineChart: echarts.ECharts | null = null;
+
+// 加载头部卡片统计数据
+const loadSummaryStats = async () => {
+  loadingStats.value = true;
+  try {
+    const [evalRes, deptRes, userYestRes, companyRes] = await Promise.all([
+      getEvaluationTotalStats(),
+      getDeptStats(),
+      getUserYesterdayStats(),
+      getCompanyStats()
+    ]);
+    
+    // axios request wrapper in RuoYi usually returns { code: 200, data: ... }
+    // @ts-ignore
+    evaluationTotalCount.value = evalRes.data || 0;
+    // @ts-ignore
+    deptCount.value = deptRes.data || 0;
+    // @ts-ignore
+    userYesterdayCount.value = userYestRes.data || 0;
+    // @ts-ignore
+    companyUserCount.value = companyRes.data || 0;
+  } catch (error) {
+    console.error("Failed to load summary stats:", error);
+  } finally {
+    loadingStats.value = false;
   }
+};
 
-  font-family: 'open sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-  font-size: 13px;
-  color: #676a6c;
-  overflow-x: hidden;
+// 加载并渲染岗位近日发布趋势报表
+const loadPostRecentChart = async () => {
+  loadingChart.value = true;
+  try {
+    const res = await getPostRecentStats();
+    // @ts-ignore
+    const trendData: TrendData[] = res.data || [];
+    
+    const dates = trendData.map(item => item.date);
+    const counts = trendData.map(item => item.count);
 
-  ul {
-    list-style-type: none;
+    renderLineChart(dates, counts);
+  } catch (error) {
+    console.error("Failed to load post recent stats:", error);
+  } finally {
+    loadingChart.value = false;
   }
+};
 
-  h4 {
-    margin-top: 0px;
+// 初始化并渲染 ECharts 折线图
+const renderLineChart = (xAxisData: string[], seriesData: number[]) => {
+  if (!lineChartRef.value) return;
+  
+  if (!lineChart) {
+    lineChart = echarts.init(lineChartRef.value);
   }
 
-  h2 {
-    margin-top: 10px;
-    font-size: 26px;
-    font-weight: 100;
-  }
+  const option = {
+    tooltip: { trigger: 'axis' },
+    legend: { data: ['岗位发布数量'], bottom: 0 },
+    grid: { left: '3%', right: '4%', bottom: '10%', containLabel: true },
+    xAxis: {
+      type: 'category',
+      boundaryGap: false,
+      data: xAxisData
+    },
+    yAxis: { 
+      type: 'value',
+      minInterval: 1 // 强制 Y 轴只显示整数刻度
+    },
+    series: [
+      {
+        name: '岗位发布数量',
+        type: 'line',
+        smooth: true,
+        data: seriesData,
+        itemStyle: { color: '#409eff' },
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: 'rgba(64,158,255,0.4)' },
+            { offset: 1, color: 'rgba(64,158,255,0.05)' }
+          ])
+        }
+      }
+    ]
+  };
+  
+  lineChart.setOption(option);
+};
+
+const handleResize = () => {
+  lineChart?.resize();
+};
+
+onMounted(() => {
+  nextTick(() => {
+    loadSummaryStats();
+    loadPostRecentChart();
+    window.addEventListener('resize', handleResize);
+  });
+});
 
-  p {
-    margin-top: 10px;
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', handleResize);
+  lineChart?.dispose();
+});
+</script>
+
+<style lang="scss" scoped>
+.dashboard-wrapper {
+  padding: 20px;
+  background-color: #f2f3f5;
+  min-height: calc(100vh - 84px);
+  
+  .welcome-card {
+    margin-bottom: 20px;
+    border-radius: 12px;
+    background: linear-gradient(135deg, #74ebd5 0%, #9face6 100%);
+    color: #fff;
+    border: none;
+    
+    .welcome-content {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding: 10px 20px;
+      
+      .welcome-text {
+        .greeting {
+          margin: 0 0 10px 0;
+          font-size: 24px;
+          font-weight: 600;
+          color: #fff;
+        }
+        .subtitle {
+          margin: 0;
+          font-size: 14px;
+          color: rgba(255, 255, 255, 0.9);
+        }
+      }
+      
+      .welcome-icon {
+        font-size: 80px;
+        color: rgba(255, 255, 255, 0.25);
+        transform: rotate(-10deg);
+      }
+    }
+  }
 
-    b {
-      font-weight: 700;
+  .stat-cards {
+    margin-bottom: 20px;
+    
+    .stat-card {
+      border-radius: 12px;
+      margin-bottom: 20px;
+      transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
+      
+      &:hover {
+        transform: translateY(-5px);
+        box-shadow: 0 12px 24px rgba(0,0,0,0.08);
+      }
+      
+      .stat-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 15px;
+        
+        .stat-title {
+          font-size: 16px;
+          color: #606266;
+          font-weight: bold;
+        }
+      }
+      
+      .stat-body {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        
+        .stat-value {
+          font-size: 42px;
+          font-weight: bold;
+          color: #303133;
+          font-family: Arial, Helvetica, sans-serif;
+        }
+        
+        .stat-icon {
+          font-size: 48px;
+          padding: 12px;
+          background-color: rgba(0,0,0,0.03);
+          border-radius: 50%;
+        }
+      }
     }
   }
 
-  .update-log {
-    ol {
-      display: block;
-      list-style-type: decimal;
-      margin-block-start: 1em;
-      margin-block-end: 1em;
-      margin-inline-start: 0;
-      margin-inline-end: 0;
-      padding-inline-start: 40px;
+  .charts-row {
+    margin-bottom: 20px;
+  }
+
+  .chart-card {
+    border-radius: 12px;
+    margin-bottom: 20px;
+    transition: all 0.3s ease;
+    
+    &:hover {
+      box-shadow: 0 8px 24px rgba(0,0,0,0.05);
+    }
+    
+    .card-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      font-weight: bold;
+      color: #303133;
+      font-size: 16px;
+      
+      &::before {
+        content: '';
+        display: inline-block;
+        width: 4px;
+        height: 16px;
+        background-color: #409eff;
+        margin-right: 10px;
+        border-radius: 2px;
+      }
     }
   }
 }

+ 198 - 140
src/views/postManage/apply-detail.vue

@@ -1,27 +1,27 @@
 <template>
   <PageShell>
-    <div class="apply-detail-page">
+    <div class="apply-detail-page" v-loading="loading">
       <div class="apply-detail-card profile-card">
         <div class="profile-left">
-          <img class="profile-avatar" :src="profile.avatar || defaultAvatar" alt="avatar" />
-          <el-link type="primary" :underline="false">一周内到岗</el-link>
+          <img class="profile-avatar" :src="profileAvatar" alt="avatar" />
+          <el-link type="primary" :underline="false">{{ profileAvailability }}</el-link>
         </div>
         <div class="profile-main">
           <div class="profile-name-row">
-            <span class="profile-name">张三</span>
-            <span class="profile-gender">♂</span>
-            <span class="profile-gender female">♀</span>
+            <span class="profile-name">{{ student.name || '--' }}</span>
+            <span v-if="student.gender === '0'" class="profile-gender">♂</span>
+            <span v-else-if="student.gender === '1'" class="profile-gender female">♀</span>
           </div>
           <div class="profile-grid">
-            <div class="profile-item"><span>身份证号</span><span>110101213432431234</span></div>
-            <div class="profile-item"><span>联系电话</span><span>138567875678</span></div>
-            <div class="profile-item"><span>电子邮箱</span><span>wangxm@example.com</span></div>
-            <div class="profile-item"><span>求职意向</span><span>审计A,审计B</span></div>
-            <div class="profile-item"><span>求职类型</span><span>实习,兼职</span></div>
-            <div class="profile-item"><span>毕业院校</span><span>复旦大学一年级本科</span></div>
-            <div class="profile-item"><span>实习时长</span><span>6个月</span></div>
-            <div class="profile-item"><span>注册日期</span><span>2023-05-12 09:34:21</span></div>
-            <div class="profile-item"><span>最后更新</span><span>2023-08-05 15:22:10</span></div>
+            <div class="profile-item"><span>身份证号</span><span>{{ displayText(student.idCardNumber) }}</span></div>
+            <div class="profile-item"><span>联系电话</span><span>{{ displayText(student.mobile) }}</span></div>
+            <div class="profile-item"><span>电子邮箱</span><span>{{ displayText(student.email) }}</span></div>
+            <div class="profile-item"><span>求职意向</span><span>{{ displayText(student.jobIntention) }}</span></div>
+            <div class="profile-item"><span>求职类型</span><span>{{ profileJobType }}</span></div>
+            <div class="profile-item"><span>毕业院校</span><span>{{ profileSchool }}</span></div>
+            <div class="profile-item"><span>实习时长</span><span>{{ profileInternshipDuration }}</span></div>
+            <div class="profile-item"><span>注册日期</span><span>{{ formatDateTime(student.createTime) }}</span></div>
+            <div class="profile-item"><span>最后更新</span><span>{{ formatDateTime(student.updateTime) }}</span></div>
           </div>
         </div>
       </div>
@@ -37,36 +37,36 @@
         <div v-if="activeTab === 'resume'" class="detail-section-list">
           <div class="detail-section">
             <div class="section-title">教育经历</div>
-            <el-table :data="educationList" border>
+            <el-table :data="educationList" border empty-text="暂无教育经历">
               <el-table-column label="学校" prop="school" min-width="120" />
-              <el-table-column label="学历" prop="degree" min-width="120" />
+              <el-table-column label="学历" prop="education" min-width="120" />
               <el-table-column label="时间" prop="period" min-width="160" />
               <el-table-column label="专业" prop="major" min-width="120" />
-              <el-table-column label="在校经历" prop="experience" min-width="180" />
+              <el-table-column label="在校经历" prop="campusExperience" min-width="180" show-overflow-tooltip />
             </el-table>
           </div>
 
           <div class="detail-section">
             <div class="section-title">工作经历</div>
-            <el-table :data="workList" border>
+            <el-table :data="workList" border empty-text="暂无工作经历">
               <el-table-column label="公司" prop="company" min-width="140" />
               <el-table-column label="行业" prop="industry" min-width="120" />
               <el-table-column label="时间" prop="period" min-width="160" />
-              <el-table-column label="职位" prop="position" min-width="120" />
+              <el-table-column label="职位" prop="jobTitle" min-width="120" />
               <el-table-column label="所属部门" prop="department" min-width="140" />
-              <el-table-column label="工作内容" prop="content" min-width="160" />
+              <el-table-column label="工作内容" prop="workContent" min-width="160" show-overflow-tooltip />
             </el-table>
           </div>
 
           <div class="detail-section">
             <div class="section-title">项目经历</div>
-            <el-table :data="projectList" border>
-              <el-table-column label="项目" prop="project" min-width="140" />
+            <el-table :data="projectList" border empty-text="暂无项目经历">
+              <el-table-column label="项目" prop="projectName" min-width="140" />
               <el-table-column label="担任角色" prop="role" min-width="120" />
               <el-table-column label="时间" prop="period" min-width="160" />
-              <el-table-column label="描述" prop="description" min-width="160" />
-              <el-table-column label="业绩" prop="achievement" min-width="160" />
-              <el-table-column label="链接" prop="link" min-width="120" />
+              <el-table-column label="描述" prop="description" min-width="160" show-overflow-tooltip />
+              <el-table-column label="业绩" prop="achievement" min-width="160" show-overflow-tooltip />
+              <el-table-column label="链接" prop="link" min-width="160" show-overflow-tooltip />
             </el-table>
           </div>
         </div>
@@ -74,20 +74,11 @@
         <div v-else-if="activeTab === 'evaluation'" class="detail-section-list">
           <div class="detail-section">
             <div class="section-title">测评信息</div>
-            <el-table :data="evaluationList" border>
+            <el-table :data="evaluationList" border empty-text="暂无测评信息">
               <el-table-column label="名称" prop="name" min-width="140" />
               <el-table-column label="岗位" prop="post" min-width="140" />
               <el-table-column label="结果" prop="result" min-width="160" />
               <el-table-column label="测评时间" prop="evaluateTime" min-width="180" />
-              <el-table-column label="操作" min-width="180">
-                <template #default="scope">
-                  <el-button link type="primary" @click="handleOpenEvaluation(scope.row)">查看</el-button>
-                  <template v-if="scope.row.result === '未通过'">
-                    <el-button link type="primary">录用</el-button>
-                    <el-button link type="primary">不录用</el-button>
-                  </template>
-                </template>
-              </el-table-column>
             </el-table>
           </div>
         </div>
@@ -95,7 +86,7 @@
         <div v-else-if="activeTab === 'training'" class="detail-section-list">
           <div class="detail-section">
             <div class="section-title">培训信息</div>
-            <el-table :data="trainingList" border>
+            <el-table :data="trainingList" border empty-text="暂无培训信息">
               <el-table-column label="名称" prop="name" min-width="180" />
               <el-table-column label="培训方式" prop="type" min-width="140" />
               <el-table-column label="培训时间" prop="time" min-width="180" />
@@ -106,7 +97,7 @@
         <div v-else-if="activeTab === 'job'" class="detail-section-list">
           <div class="detail-section">
             <div class="section-title">任职信息</div>
-            <el-table :data="jobList" border>
+            <el-table :data="jobList" border empty-text="暂无任职信息">
               <el-table-column label="公司" prop="company" min-width="140" />
               <el-table-column label="岗位" prop="post" min-width="120" />
               <el-table-column label="岗位类型" prop="postType" min-width="140" />
@@ -114,106 +105,197 @@
               <el-table-column label="入职时间" prop="entryTime" min-width="160" />
               <el-table-column label="离职时间" prop="leaveTime" min-width="160" />
               <el-table-column label="离职原因" prop="leaveReason" min-width="140" />
-              <el-table-column label="操作" min-width="140">
-                <template #default>
-                  <el-button link type="primary" @click="handleOpenCompanyRate">查看该公司评价</el-button>
-                </template>
-              </el-table-column>
             </el-table>
           </div>
         </div>
-
-        <el-dialog v-model="companyRateDialog.visible" title="评价" width="460px" append-to-body>
-          <div class="company-rate-content">
-            <div class="company-rate-item">
-              <div class="company-rate-head">
-                <span class="company-rate-label">综合评价</span>
-                <el-rate v-model="companyRateDialog.form.totalRate" disabled show-score text-color="#303133" score-template="{value}星" />
-              </div>
-              <el-input v-model="companyRateDialog.form.totalRemark" type="textarea" :rows="3" readonly />
-            </div>
-            <div class="company-rate-item">
-              <div class="company-rate-head">
-                <span class="company-rate-label">能力A</span>
-                <el-rate v-model="companyRateDialog.form.abilityARate" disabled show-score text-color="#303133" score-template="{value}星" />
-              </div>
-              <el-input v-model="companyRateDialog.form.abilityARemark" type="textarea" :rows="3" readonly />
-            </div>
-            <div class="company-rate-item">
-              <div class="company-rate-head">
-                <span class="company-rate-label">能力B</span>
-                <el-rate v-model="companyRateDialog.form.abilityBRate" disabled show-score text-color="#303133" score-template="{value}星" />
-              </div>
-              <el-input v-model="companyRateDialog.form.abilityBRemark" type="textarea" :rows="3" readonly />
-            </div>
-          </div>
-        </el-dialog>
       </div>
     </div>
   </PageShell>
 </template>
 
 <script setup name="PostManageApplyDetail" lang="ts">
-import { reactive, ref } from 'vue';
-import { useRouter } from 'vue-router';
+import { computed, getCurrentInstance, onMounted, ref, type ComponentInternalInstance } from 'vue';
+import { useRoute } from 'vue-router';
 import PageShell from '@/components/PageShell/index.vue';
 import defaultAvatar from '@/assets/images/user.jpg';
-
-const router = useRouter();
+import { getStudentInfo, type MainStudentEducationVO, type MainStudentExperienceVO, type MainStudentProjectVO, type MainStudentVO, type MainExamApplyRecordVO, type StudentTrainingRecordVO, type StudentJobRecordVO } from '@/api/main/student';
+import { getDicts } from '@/api/system/dict/data';
+import type { DictDataVO } from '@/api/system/dict/data/types';
+import { parseTime } from '@/utils/ruoyi';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const route = useRoute();
+const loading = ref(false);
 const activeTab = ref('resume');
+const student = ref<MainStudentVO>({});
+const jobTypeOptions = ref<DictDataVO[]>([]);
+const educationOptions = ref<DictDataVO[]>([]);
+const gradeOptions = ref<DictDataVO[]>([]);
+const internshipDurationOptions = ref<DictDataVO[]>([]);
+const arrivalTimeOptions = ref<DictDataVO[]>([]);
+
+const displayText = (value?: string | number | null) => {
+  if (value === null || value === undefined) {
+    return '--';
+  }
+  const text = String(value).trim();
+  return text ? text : '--';
+};
 
-const profile = {
-  avatar: ''
+const getDictLabel = (options: DictDataVO[], value?: string | null) => {
+  if (!value) {
+    return '';
+  }
+  return options.find((item) => item.dictValue === value)?.dictLabel || value;
 };
 
-const educationList = [
-  { school: '复旦大学', degree: '本科-全日制', period: '2022.9——2026.7', major: '会计', experience: '在校经历XXXXXXX' }
-];
-
-const workList = [
-  { company: 'XXXX公司', industry: '服务业', period: '2022.9——2026.7(实习)', position: '会计', department: '市场部', content: '工作内容XXXXXXXX' }
-];
-
-const projectList = [
-  { project: 'XXXX项目', role: '负责人', period: '2022.9——2026.7', description: '描述XXXX', achievement: '业绩XXXXXX', link: 'https://qq.cn' }
-];
-
-const evaluationList = [
-  { name: '审计A1测试', post: '审计A1', result: '通过', evaluateTime: '2025.12.12 12:12' },
-  { name: '', post: '', result: '未通过', evaluateTime: '' }
-];
-
-const trainingList = [{ name: '审计A1培训', type: '线下', time: '2025.12.12 12:12' }];
-
-const jobList = [
-  { company: 'XXXXX公司', post: '审计A', postType: '全职、实习、兼职', status: '在职', entryTime: '2025.12.12', leaveTime: '', leaveReason: '' },
-  { company: 'XXXXX公司', post: '审计A', postType: '全职', status: '离职', entryTime: '2024.12.12', leaveTime: '2025.12.01', leaveReason: '个人发展' }
-];
-
-const companyRateDialog = reactive({
-  visible: false,
-  form: {
-    totalRate: 3.5,
-    totalRemark: '在职期间表现佳',
-    abilityARate: 3,
-    abilityARemark: '在职期间表现佳',
-    abilityBRate: 3,
-    abilityBRemark: '在职期间表现佳'
+const getMultiDictLabel = (options: DictDataVO[], value?: string | null) => {
+  if (!value) {
+    return '--';
+  }
+  const values = value
+    .split(',')
+    .map((item) => item.trim())
+    .filter(Boolean);
+  if (!values.length) {
+    return '--';
   }
+  return values.map((item) => getDictLabel(options, item)).join('、');
+};
+
+const formatDateTime = (value?: string | null) => parseTime(value, '{y}-{m}-{d} {h}:{i}:{s}') || '--';
+
+const formatPeriod = (startTime?: string, endTime?: string) => {
+  const start = (startTime || '').trim();
+  const end = (endTime || '').trim();
+  if (start && end) {
+    return `${start} - ${end}`;
+  }
+  return start || end || '--';
+};
+
+const profileAvatar = computed(() => student.value.avatarUrl || defaultAvatar);
+const profileAvailability = computed(() => getDictLabel(arrivalTimeOptions.value, student.value.availability) || displayText(student.value.availability));
+const profileJobType = computed(() => getMultiDictLabel(jobTypeOptions.value, student.value.jobType));
+const profileInternshipDuration = computed(
+  () => getDictLabel(internshipDurationOptions.value, student.value.internshipDuration) || displayText(student.value.internshipDuration)
+);
+const profileSchool = computed(() => {
+  const schoolName = student.value.schoolName?.trim() || '';
+  const education = getDictLabel(educationOptions.value, student.value.education);
+  const grade = getDictLabel(gradeOptions.value, student.value.grade);
+  return [schoolName, education, grade].filter(Boolean).join(' ') || '--';
 });
 
-const handleOpenEvaluation = (row: { name: string }) => {
-  router.push({
-    path: '/evaluation/detail',
-    query: {
-      name: row.name
+const educationList = computed(() =>
+  (student.value.educationList || []).map((item: MainStudentEducationVO) => ({
+    ...item,
+    education: getDictLabel(educationOptions.value, item.education) || displayText(item.education),
+    period: formatPeriod(item.startTime, item.endTime),
+    campusExperience: displayText(item.campusExperience)
+  }))
+);
+
+const workList = computed(() =>
+  (student.value.experienceList || []).map((item: MainStudentExperienceVO) => ({
+    ...item,
+    period: item.isInternship === 1 ? `${formatPeriod(item.startTime, item.endTime)}(实习)` : formatPeriod(item.startTime, item.endTime),
+    workContent: displayText(item.workContent)
+  }))
+);
+
+const projectList = computed(() =>
+  (student.value.projectList || []).map((item: MainStudentProjectVO) => ({
+    ...item,
+    period: formatPeriod(item.startTime, item.endTime),
+    description: displayText(item.description),
+    achievement: displayText(item.achievement),
+    link: displayText(item.link)
+  }))
+);
+
+const evaluationList = computed(() =>
+  (student.value.evaluationList || []).map((item: MainExamApplyRecordVO) => ({
+    name: displayText(item.evaluationName),
+    post: displayText(item.positionName),
+    result: displayText(item.statusText || item.finalResult),
+    evaluateTime: formatDateTime(item.finishedTime || item.createTime)
+  }))
+);
+
+const trainingList = computed(() =>
+  (student.value.trainingList || []).map((item: StudentTrainingRecordVO) => {
+    // 培训方式
+    let type = '--';
+    if (item.trainingType === 'video') {
+      type = item.progress === 100 ? '线上(已完成)' : item.progress != null ? `线上(学习中 ${item.progress}%)` : '线上';
+    } else if (item.trainingType === 'offline') {
+      type = '线下';
+      if (item.enrollStatus === 3) type = '线下(已签到)';
+      else if (item.enrollStatus === 1) type = '线下(已通过)';
+      else if (item.enrollStatus === 0) type = '线下(待审核)';
+    } else if (item.trainingType === 'live') {
+      type = '直播';
     }
-  });
+    // 培训时间
+    const time = formatPeriod(
+      item.trainingStartTime?.split(' ')[0] || '',
+      item.trainingEndTime?.split(' ')[0] || ''
+    );
+    return {
+      name: displayText(item.name),
+      type,
+      time
+    };
+  })
+);
+
+const jobList = computed(() =>
+  (student.value.jobList || []).map((item: StudentJobRecordVO) => ({
+    company: displayText(item.companyName),
+    post: displayText(item.postName),
+    postType: displayText(item.postType),
+    status: item.employmentStatus === 'onboard' ? '在职' : item.employmentStatus === 'left' ? '已离职' : displayText(item.employmentStatus),
+    entryTime: formatDateTime(item.entryTime),
+    leaveTime: formatDateTime(item.leaveTime),
+    leaveReason: displayText(item.leaveReason)
+  }))
+);
+
+const loadDicts = async () => {
+  const [jobTypeRes, educationRes, gradeRes, internshipDurationRes, arrivalTimeRes] = await Promise.all([
+    getDicts('main_position_type'),
+    getDicts('main_education'),
+    getDicts('main_experience'),
+    getDicts('main_internship_duration'),
+    getDicts('main_arrival_time')
+  ]);
+  jobTypeOptions.value = jobTypeRes.data || [];
+  educationOptions.value = educationRes.data || [];
+  gradeOptions.value = gradeRes.data || [];
+  internshipDurationOptions.value = internshipDurationRes.data || [];
+  arrivalTimeOptions.value = arrivalTimeRes.data || [];
 };
 
-const handleOpenCompanyRate = () => {
-  companyRateDialog.visible = true;
+const loadStudentDetail = async () => {
+  const studentId = route.query.studentId as string | undefined;
+  if (!studentId) {
+    proxy?.$modal.msgWarning('学员ID不能为空');
+    student.value = {};
+    return;
+  }
+  loading.value = true;
+  try {
+    const res = await getStudentInfo(studentId);
+    student.value = res.data || {};
+  } finally {
+    loading.value = false;
+  }
 };
+
+onMounted(async () => {
+  await Promise.all([loadDicts(), loadStudentDetail()]);
+});
 </script>
 
 <style scoped>
@@ -308,28 +390,4 @@ const handleOpenCompanyRate = () => {
   font-weight: 600;
   color: #303133;
 }
-
-.company-rate-content {
-  display: flex;
-  flex-direction: column;
-  gap: 16px;
-}
-
-.company-rate-item {
-  display: flex;
-  flex-direction: column;
-  gap: 10px;
-}
-
-.company-rate-head {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-}
-
-.company-rate-label {
-  width: 56px;
-  color: #303133;
-  flex-shrink: 0;
-}
 </style>

+ 76 - 21
src/views/postManage/apply-list.vue

@@ -32,7 +32,7 @@
           </el-form-item>
           <div class="apply-toolbar-actions">
             <el-button disabled>录用</el-button>
-            <el-button type="primary">导出列表</el-button>
+            <el-button type="primary" @click="handleExport">导出列表</el-button>
           </div>
         </el-form>
 
@@ -55,10 +55,10 @@
           <el-table-column label="测评时间" prop="evaluateTime" min-width="180" align="center" />
           <el-table-column label="操作" min-width="220" align="center">
             <template #default="scope">
-              <template v-if="scope.row.statusText !== '学员已拒绝'">
-                <el-button link type="primary">测评详情</el-button>
+              <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>
-                <template v-if="scope.row.statusText === '待回复'">
+                <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>
                 </template>
@@ -79,11 +79,16 @@
       <el-dialog v-model="hireDialog.visible" title="审核说明" width="680px" append-to-body>
         <div class="result-dialog-content">
           <div class="result-line"><span class="result-label">结果:</span><span class="result-value">录用</span></div>
-          <div class="result-upload-row">
-            <el-button>上传文件</el-button>
-          </div>
-          <div class="result-file-list">
-            <div v-for="item in hireDialog.files" :key="item" class="result-file-item">{{ item }}</div>
+          <div class="result-remark-row">
+            <span class="result-label top">附件</span>
+            <div class="result-upload-panel">
+              <FileUpload
+                v-model="hireDialog.offerAttachment"
+                :limit="5"
+                :file-type="['png', 'jpg', 'jpeg', 'doc', 'docx', 'xlsx', 'xls', 'ppt', 'pptx', 'txt', 'pdf']"
+                :file-size="20"
+              />
+            </div>
           </div>
           <div class="result-remark-row">
             <span class="result-label top">说明</span>
@@ -121,13 +126,19 @@
 <script setup name="PostManageApplyList" lang="ts">
 import { computed, getCurrentInstance, onMounted, reactive, ref, type ComponentInternalInstance } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
-import { listPostCandidates, updatePostCandidateStatus, type PostCandidateVO } from '@/api/main/postManage/candidate';
+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';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const router = useRouter();
 const route = useRoute();
 const modal = proxy?.$modal as any;
+const STATUS_ADOPTED = 'adopted';
+const STATUS_REJECTED = 'rejected';
+const STATUS_PENDING = 'pending';
+const STATUS_CLOSED = 'closed';
+
 const pageTitle = computed(() => {
   const postName = route.query.postName as string | undefined;
   return postName ? `${postName} 报名列表` : '报名列表';
@@ -145,10 +156,10 @@ const loading = ref(false);
 const total = ref(0);
 
 const applyStatusOptions = [
-  { label: '录用', value: 'adopted' },
-  { label: '不录用', value: 'rejected' },
-  { label: '待回复', value: 'pending' },
-  { label: '学员已拒绝', value: 'closed' }
+  { label: '录用', value: STATUS_ADOPTED },
+  { label: '不录用', value: STATUS_REJECTED },
+  { label: '待回复', value: STATUS_PENDING },
+  { label: '学员已拒绝', value: STATUS_CLOSED }
 ];
 
 const abilityOptions = ['测评能力'];
@@ -159,7 +170,7 @@ const hireDialog = reactive({
   visible: false,
   remark: '',
   agree: true,
-  files: ['文档.docx', '附件.png(上传...)', '组件123.jpg'],
+  offerAttachment: '',
   currentId: '',
   submitting: false
 });
@@ -204,9 +215,27 @@ const handleBack = () => {
   router.push((route.query.backPath as string) || '/postManage');
 };
 
+const handleExport = () => {
+  const postId = route.query.postId as string | undefined;
+  if (!postId) {
+    modal?.msgWarning('岗位ID不能为空');
+    return;
+  }
+  proxy?.download(
+    '/main/postCandidate/export',
+    {
+      postId,
+      keyword: queryParams.keyword,
+      status: queryParams.status
+    },
+    `post_candidate_${new Date().getTime()}.xlsx`
+  );
+};
+
 const handleHire = (row: PostCandidateVO) => {
   hireDialog.currentId = String(row.id);
   hireDialog.remark = row.remark || '';
+  hireDialog.offerAttachment = '';
   hireDialog.agree = true;
   hireDialog.visible = true;
 };
@@ -219,12 +248,20 @@ const handleReject = (row: PostCandidateVO) => {
 
 const submitHire = async () => {
   if (!hireDialog.currentId) return;
+  if (!hireDialog.agree) {
+    modal?.msgWarning('请先阅读并同意免责协议');
+    return;
+  }
+  if (!hireDialog.offerAttachment) {
+    modal?.msgWarning('请先上传录用附件');
+    return;
+  }
   hireDialog.submitting = true;
   try {
-    await updatePostCandidateStatus({
+    await hirePostCandidate({
       id: hireDialog.currentId,
-      status: 'adopted',
-      remark: hireDialog.remark
+      remark: hireDialog.remark.trim(),
+      offerAttachment: hireDialog.offerAttachment
     });
     hireDialog.visible = false;
     modal?.msgSuccess('录用成功');
@@ -240,8 +277,8 @@ const submitReject = async () => {
   try {
     await updatePostCandidateStatus({
       id: rejectDialog.currentId,
-      status: 'rejected',
-      remark: rejectDialog.remark
+      status: STATUS_REJECTED,
+      remark: rejectDialog.remark.trim()
     });
     rejectDialog.visible = false;
     modal?.msgSuccess('操作成功');
@@ -257,7 +294,21 @@ const handleOpenDetail = (row: { id: number; name: string }) => {
     query: {
       applyId: String(row.id),
       name: row.name,
-      postId: String(route.query.postId || '')
+      postId: String(route.query.postId || ''),
+      studentId: String((row as PostCandidateVO).studentId || '')
+    }
+  });
+};
+
+const handleEvaluationDetail = (row: PostCandidateVO) => {
+  router.push({
+    path: '/postManage/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 || '')
     }
   });
 };
@@ -429,4 +480,8 @@ onMounted(() => {
 .result-remark-row.compact :deep(.el-textarea) {
   flex: 1;
 }
+
+.result-upload-panel {
+  flex: 1;
+}
 </style>

+ 34 - 8
src/views/postManage/create.vue

@@ -239,7 +239,7 @@ const isEdit = computed(() => Boolean(editId.value));
 const pageTitle = computed(() => (isEdit.value ? '编辑岗位' : '添加岗位'));
 const submitButtonText = computed(() => (isEdit.value ? '保存并提交审核' : '提交审核'));
 const regionOptions = regionData as unknown as CascaderOption[];
-const regionProps = { value: 'code', label: 'label', children: 'children' };
+const regionProps = { value: 'value', label: 'label', children: 'children' };
 const postNameOptions = ref<CascaderOption[]>([]);
 const postNameProps = { value: 'value', label: 'label', children: 'children', emitPath: true, checkStrictly: false };
 const jobTypeOptions = ref<DictDataVO[]>([]);
@@ -291,7 +291,31 @@ const form = reactive<PostCreateFormModel>({
   remark: ''
 });
 
-const rules = reactive({
+const validateRecruitCount = (rule: any, value: any, callback: any) => {
+  if (!form.unlimitedRecruitCount && !form.recruitCount) {
+    callback(new Error('招聘人数不能为空'));
+  } else {
+    callback();
+  }
+};
+
+const validateApplyTime = (rule: any, value: any, callback: any) => {
+  if (!form.unlimitedApplyTime && form.applyTimeRange.length !== 2) {
+    callback(new Error('报名时间不能为空'));
+  } else {
+    callback();
+  }
+};
+
+const validatePassingScores = (rule: any, value: any, callback: any) => {
+  if (form.passingScores.some((item) => !item.score)) {
+    callback(new Error('及格线不能为空'));
+  } else {
+    callback();
+  }
+};
+
+const rules = reactive<any>({
   postNamePath: [{ required: true, message: '岗位名称不能为空', trigger: 'change' }],
   regionCodes: [{ required: true, message: '地区不能为空', trigger: 'change' }],
   addressDetail: [{ required: true, message: '详细地址不能为空', trigger: 'blur' }],
@@ -301,7 +325,10 @@ const rules = reactive({
   postCode: [{ required: true, message: '岗位编码不能为空', trigger: 'blur' }],
   postCategory: [{ required: true, message: '岗位级别不能为空', trigger: 'change' }],
   evaluationDuration: [{ required: true, message: '测评时长不能为空', trigger: 'blur' }],
-  postSort: [{ required: true, message: '岗位顺序不能为空', trigger: 'blur' }]
+  postSort: [{ required: true, message: '岗位顺序不能为空', trigger: 'blur' }],
+  recruitCount: [{ required: true, validator: validateRecruitCount, trigger: ['blur', 'change'] }],
+  applyTimeRange: [{ required: true, validator: validateApplyTime, trigger: ['blur', 'change'] }],
+  passingScores: [{ required: true, validator: validatePassingScores, trigger: ['blur', 'change'] }]
 });
 
 const regionText = computed(() =>
@@ -537,22 +564,21 @@ const loadDetail = async () => {
 
 const handleRecruitUnlimitedChange = (value: boolean) => {
   if (value) form.recruitCount = undefined;
+  formRef.value?.validateField('recruitCount');
 };
 
 const handleApplyUnlimitedChange = (value: boolean) => {
   if (value) form.applyTimeRange = [];
+  formRef.value?.validateField('applyTimeRange');
 };
 
 const validateStep = async () => {
   if (activeStep.value === 0) {
-    await formRef.value?.validateField(['postNamePath', 'postCode', 'regionCodes', 'addressDetail', 'jobType', 'salaryMin', 'salaryMax']);
+    await formRef.value?.validateField(['postNamePath', 'postCode', 'regionCodes', 'addressDetail', 'jobType', 'salaryMin', 'salaryMax', 'recruitCount', 'applyTimeRange']);
     if (Number(form.salaryMin) > Number(form.salaryMax)) throw new Error('最低薪资不能大于最高薪资');
-    if (!form.unlimitedRecruitCount && !form.recruitCount) throw new Error('招聘人数不能为空');
-    if (!form.unlimitedApplyTime && form.applyTimeRange.length !== 2) throw new Error('报名时间不能为空');
   }
   if (activeStep.value === 1) {
-    await formRef.value?.validateField(['postCategory', 'evaluationDuration']);
-    if (form.passingScores.some((item) => !item.score)) throw new Error('及格线不能为空');
+    await formRef.value?.validateField(['postCategory', 'evaluationDuration', 'passingScores']);
   }
 };
 

+ 171 - 74
src/views/postManage/evaluation-view.vue

@@ -1,115 +1,207 @@
 <template>
   <PageShell>
-    <div class="evaluation-view-page">
+    <div class="evaluation-view-page" v-loading="loading">
       <div class="left-card">
         <div class="card-title">考生信息</div>
         <div class="info-list">
-          <div class="info-item"><span>姓名:</span><span>张三</span></div>
-          <div class="info-item"><span>报名岗位:</span><span>审计</span></div>
-          <div class="info-item"><span>开始时间:</span><span>2024-06-13 09:49:47</span></div>
-          <div class="info-item"><span>结束时间:</span><span>2024-06-13 09:50:26</span></div>
-          <div class="info-item"><span>答题时长:</span><span>0分39秒</span></div>
-          <div class="info-item"><span>浏览器:</span><span>unknown Browser</span></div>
-          <div class="info-item"><span>终端设备:</span><span>iPhone iOS 17.5.1</span></div>
-          <div class="info-item"><span>用户IP:</span><span>59.46.39.185</span></div>
+          <div class="info-item"><span>姓名:</span><span>{{ studentName }}</span></div>
+          <div class="info-item"><span>报名岗位:</span><span>{{ positionName }}</span></div>
         </div>
 
         <div class="card-title second">答题信息</div>
         <div class="answer-list">
-          <div class="answer-item"><span>能力测试:10/30</span><el-tag type="success">通过</el-tag></div>
-          <div class="answer-item"><span>实操能力:20/40</span><el-tag type="success">通过</el-tag></div>
-          <div class="answer-item"><span>性格:12/34</span><el-tag type="danger">未通过</el-tag></div>
+          <div v-for="ab in scores" :key="ab.abilityName" class="answer-item">
+            <span>{{ ab.abilityName }}:{{ ab.score }}/{{ ab.totalScore }}</span>
+            <el-tag :type="ab.pass ? 'success' : 'danger'">{{ ab.pass ? '通过' : '未通过' }}</el-tag>
+          </div>
+          <div v-if="!scores.length" class="answer-item" style="color:#999">暂无答题记录</div>
         </div>
 
         <div class="card-title second">维度分析</div>
-        <div class="chart-placeholder">维度图</div>
+        <div class="chart-placeholder">暂不支持</div>
         <div class="footer-btn">
           <el-button @click="handleBack">返回</el-button>
-          <el-button type="primary">下一份</el-button>
         </div>
       </div>
 
       <div class="right-card">
-        <div v-for="item in questionList" :key="item.id" class="question-block">
-          <div class="question-head">
-            <span>第{{ item.id }}题</span>
-            <div class="question-result">
-              <el-tag :type="item.correct ? 'success' : 'danger'">{{ item.correct ? '正确' : '错误' }}</el-tag>
-              <span>得分</span>
-              <el-input :model-value="String(item.score)" class="score-input" readonly />
+        <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>
+              <div class="question-result">
+                <el-tag :type="item.correct ? 'success' : 'danger'">{{ item.correct ? '正确' : '错误' }}</el-tag>
+                <span>得分</span>
+                <el-input :model-value="String(item.score)" class="score-input" readonly />
+              </div>
             </div>
-          </div>
-          <div class="question-title">请选择一个选项</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 }}.{{ option.text }}</el-radio>
-              <span v-if="option.correct" class="correct-text">正确答案</span>
+            <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>
+            </div>
+            <div v-if="item.analysis" class="question-analysis">
+              <div>答案解析:</div>
+              <div v-html="item.analysis"></div>
             </div>
           </div>
-          <div v-if="item.analysis" class="question-analysis">
-            <div>答案解析:</div>
-            <div>{{ item.analysis }}</div>
-          </div>
-        </div>
+        </template>
+        <el-empty v-else description="暂无答题详情数据" />
       </div>
     </div>
   </PageShell>
 </template>
 
 <script setup name="PostManageEvaluationView" lang="ts">
-import { getCurrentInstance, type ComponentInternalInstance } from 'vue';
-import { useRouter } from 'vue-router';
+import { getCurrentInstance, type ComponentInternalInstance, ref, onMounted } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
 import PageShell from '@/components/PageShell/index.vue';
+import { getEvaluation, getExamScoreList, getExamPaperQuestions, getExamAnswerList } from '@/api/main/evaluation/index';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const router = useRouter();
+const route = useRoute();
+
+const loading = ref(false);
+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 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 questionList = [
-  {
-    id: 1,
-    correct: true,
-    score: 5,
-    answer: 'A',
-    options: [
-      { label: 'A', text: '选项1', correct: true },
-      { label: 'B', text: '选项2', correct: false },
-      { label: 'C', text: '选项3', correct: false },
-      { label: 'D', text: '选项4', correct: false }
-    ],
-    analysis: ''
-  },
-  {
-    id: 2,
-    correct: true,
-    score: 5,
-    answer: 'A',
-    options: [
-      { label: 'A', text: '选项1', correct: true },
-      { label: 'B', text: '选项2', correct: false },
-      { label: 'C', text: '选项3', correct: false },
-      { label: 'D', text: '选项4', correct: false }
-    ],
-    analysis: ''
-  },
-  {
-    id: 3,
-    correct: false,
-    score: 0,
-    answer: 'C',
-    options: [
-      { label: 'A', text: '选项1', correct: true },
-      { label: 'B', text: '选项2', correct: false },
-      { label: 'C', text: '选项3', correct: false },
-      { label: 'D', text: '选项4', correct: false }
-    ],
-    analysis: '解析解析解析解析解析解析解析解析'
+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;
   }
-];
+  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)))
+  );
+};
+
+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 extractAnswers = (ansData: any) => {
+    let answers = ansData?.details || ansData?.answerList || ansData?.answers || ansData?.questionList || ansData?.data || [];
+    return Array.isArray(answers) ? answers : [];
+};
+
+const combineQA = (questions: any[], answers: 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) {}
+    }
+    
+    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;
+    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;
+    }
+
+    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 || ''
+    };
+  });
+};
+
+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));
+        }
+
+        scores.value = scoreItems;
+        questionList.value = allQuestions;
+
+    } catch (e) {
+        console.error(e);
+        proxy?.$modal?.msgError('获取测评数据失败');
+    } finally {
+        loading.value = false;
+    }
+};
 
 const handleBack = () => {
   proxy?.$tab.closePage();
   router.back();
 };
+
+onMounted(() => {
+    loadData();
+});
 </script>
 
 <style scoped>
@@ -218,4 +310,9 @@ const handleBack = () => {
   color: #606266;
   font-size: 13px;
 }
+</style>
+  margin-top: 16px;
+  color: #606266;
+  font-size: 13px;
+}
 </style>

+ 4 - 0
test.mjs

@@ -0,0 +1,4 @@
+import { regionData, codeToText } from 'element-china-area-data';
+console.log('codeToText keys:', Object.keys(codeToText).slice(0, 5));
+console.log('codeToText["11"]:', codeToText['11']);
+console.log('codeToText["1101"]:', codeToText['1101']);