zlwu 1 год назад
Родитель
Сommit
0f199aa50d

+ 1 - 1
backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java

@@ -297,7 +297,7 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
     @Override
     public List<SysUserVo> selectUserByIds(List<Long> userIds, Long deptId) {
         return baseMapper.selectUserList(new LambdaQueryWrapper<SysUser>()
-                .select(SysUser::getUserId, SysUser::getUserName, SysUser::getNickName)
+                .select(SysUser::getUserId, SysUser::getUserName, SysUser::getNickName, SysUser::getPhonenumber)
                 .eq(SysUser::getStatus, SystemConstants.NORMAL)
                 .eq(ObjectUtil.isNotNull(deptId), SysUser::getDeptId, deptId)
                 .in(CollUtil.isNotEmpty(userIds), SysUser::getUserId, userIds));

+ 28 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentLeaveController.java

@@ -1,6 +1,8 @@
 package org.dromara.xg.controller;
 
+import java.time.LocalDateTime;
 import java.util.List;
+import java.util.Map;
 
 import lombok.RequiredArgsConstructor;
 import jakarta.servlet.http.HttpServletResponse;
@@ -101,4 +103,30 @@ public class XgStudentLeaveController extends BaseController {
     public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
         return toAjax(xgStudentLeaveService.deleteWithValidByIds(List.of(ids), true));
     }
+
+    /**
+     * 根据学生ID列表和检查日期查询学生请假信息
+     *
+     * @param studentIds 学生ID列表(逗号分隔)
+     * @param checkDate  检查日期
+     */
+    @SaCheckPermission("studentLeave:studentLeave:query")
+    @GetMapping("/queryByCheckDate")
+    public R<Map<Long, XgStudentLeaveVo>> queryByCheckDate(
+            @RequestParam String studentIds,
+            @RequestParam String checkDate) {
+        try {
+            List<Long> studentIdList = List.of(studentIds.split(","))
+                    .stream()
+                    .map(String::trim)
+                    .map(Long::valueOf)
+                    .toList();
+            LocalDateTime checkDateTime = LocalDateTime.parse(checkDate.replace(" ", "T"));
+            Map<Long, XgStudentLeaveVo> result = xgStudentLeaveService.queryStudentLeavesByCheckDate(studentIdList,
+                    checkDateTime);
+            return R.ok(result);
+        } catch (Exception e) {
+            return R.fail("参数格式错误:" + e.getMessage());
+        }
+    }
 }

+ 11 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentLeaveService.java

@@ -5,8 +5,10 @@ import org.dromara.xg.domain.bo.XgStudentLeaveBo;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.mybatis.core.page.PageQuery;
 
+import java.time.LocalDateTime;
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 学生请假 - 记录学生的请假申请、审批和销假信息Service接口
@@ -65,4 +67,13 @@ public interface IXgStudentLeaveService {
      * @return 是否删除成功
      */
     Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
+
+    /**
+     * 根据学生ID列表和检查日期查询学生请假信息
+     *
+     * @param studentIds 学生ID列表
+     * @param checkDate  检查日期
+     * @return 学生ID -> 请假信息的映射
+     */
+    Map<Long, XgStudentLeaveVo> queryStudentLeavesByCheckDate(List<Long> studentIds, LocalDateTime checkDate);
 }

+ 34 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/impl/XgStudentLeaveServiceImpl.java

@@ -51,6 +51,7 @@ import org.dromara.xg.domain.XgStudentManager;
 import org.dromara.xg.domain.XgStudentManagerSource;
 import org.dromara.xg.domain.XgClassMsg;
 
+import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Map;
 import java.util.Collection;
@@ -582,4 +583,37 @@ public class XgStudentLeaveServiceImpl implements IXgStudentLeaveService {
         baseMapper.updateById(xgStudentLeave);
     }
 
+    /**
+     * 根据学生ID列表和检查日期查询学生请假信息
+     *
+     * @param studentIds 学生ID列表
+     * @param checkDate 检查日期
+     * @return 学生ID -> 请假信息的映射
+     */
+    @Override
+    public Map<Long, XgStudentLeaveVo> queryStudentLeavesByCheckDate(List<Long> studentIds, LocalDateTime checkDate) {
+        if (studentIds == null || studentIds.isEmpty() || checkDate == null) {
+            return Map.of();
+        }
+
+        LambdaQueryWrapper<XgStudentLeave> lqw = Wrappers.lambdaQuery();
+        lqw.in(XgStudentLeave::getStudentId, studentIds);
+        // 查询审核状态为"waiting"(待审核)或"finish"(已完成)的请假记录
+        lqw.in(XgStudentLeave::getApprovalStatus, "waiting", "finish");
+        // 检查日期在请假时间段内
+        lqw.le(XgStudentLeave::getStartTime, checkDate);
+        lqw.ge(XgStudentLeave::getEndTime, checkDate);
+        lqw.orderByDesc(XgStudentLeave::getCreateTime);
+
+        List<XgStudentLeave> leaveList = baseMapper.selectList(lqw);
+
+        // 转换为VO并按学生ID分组,每个学生只取最新的一条请假记录
+        return leaveList.stream()
+            .collect(Collectors.toMap(
+                XgStudentLeave::getStudentId,
+                leave -> MapstructUtils.convert(leave, XgStudentLeaveVo.class),
+                (existing, replacement) -> existing // 保留第一条(最新的)
+            ));
+    }
+
 }

+ 186 - 22
web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/DormHygieneCheckEditDialog.vue

@@ -204,8 +204,7 @@
             </div>
 
             <div class="student-details">
-              <p><strong>班级:</strong>{{ student.className }}</p>
-              <p><strong>专业:</strong>{{ student.majorName }}</p>
+              <p><strong>班级:</strong>{{ student.className }} <strong>专业:</strong>{{ student.majorName }}</p>
             </div>
 
             <div class="student-score-section">
@@ -249,6 +248,36 @@
               </el-row>
             </div>
 
+            <!-- 请假信息区域 -->
+            <div class="leave-section" v-if="getStudentLeave(student.studentId)">
+              <div class="leave-header">
+                <span>请假信息</span>
+                <div class="leave-header-actions">
+                  <el-button size="small" plain @click="viewLeaveProcess(student, getStudentLeave(student.studentId))" title="查看请假流程">
+                    查看流程
+                  </el-button>
+                  <el-tag type="warning" size="small">请假中</el-tag>
+                </div>
+              </div>
+              <div class="leave-info">
+                <div class="leave-item">
+                  <span class="leave-label">发起时间:</span>
+                  <span class="leave-value">{{ formatDateTime(getStudentLeave(student.studentId)?.createTime) }}</span>
+                </div>
+                <div class="leave-item">
+                  <span class="leave-label">请假时间:</span>
+                  <span class="leave-value"
+                    >{{ formatDateTime(getStudentLeave(student.studentId)?.startTime) }} 至
+                    {{ formatDateTime(getStudentLeave(student.studentId)?.endTime) }}</span
+                  >
+                </div>
+                <div class="leave-item">
+                  <span class="leave-label">请假类型:</span>
+                  <span class="leave-value">{{ getStudentLeave(student.studentId)?.leaveTypeName }}</span>
+                </div>
+              </div>
+            </div>
+
             <!-- 违纪录入区域 -->
             <div class="violation-section">
               <div class="violation-header">
@@ -315,17 +344,30 @@
       @confirm="handleViolationConfirm"
       @close="handleViolationClose"
     />
+
+    <!-- 请假流程查看弹窗 -->
+    <LeaveProcessDialog
+      ref="leaveProcessDialogRef"
+      :visible="leaveProcessDialogVisible"
+      :student-info="currentLeaveStudent"
+      :leave-info="currentLeaveInfo"
+      @update:visible="leaveProcessDialogVisible = $event"
+      @close="handleLeaveProcessClose"
+    />
   </el-dialog>
 </template>
 
 <script setup name="DormHygieneCheckEditDialog" lang="ts">
-import { ref, reactive, computed, watch, toRefs, getCurrentInstance, type ComponentInternalInstance } from 'vue';
-import { Plus, InfoFilled, UploadFilled, Document, Loading, Download, Delete } from '@element-plus/icons-vue';
+import { ref, reactive, computed, watch, toRefs, getCurrentInstance, nextTick, type ComponentInternalInstance } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { Plus, InfoFilled, UploadFilled, Document, Loading, Download, Delete, View } from '@element-plus/icons-vue';
 import { getToken } from '@/utils/auth';
 import ImagePreview from '@/components/ImagePreview/index.vue';
 import request from '@/utils/request';
-// @ts-ignore
+// @ts-expect-error - Component import without type declarations
 import ViolationRecordDialog from './components/ViolationRecordDialog.vue';
+// @ts-expect-error - Component import without type declarations
+import LeaveProcessDialog from './components/LeaveProcessDialog.vue';
 
 // 组件实例
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -410,12 +452,21 @@ const studentScores = ref<any[]>([]);
 // 违纪记录数据
 const violationRecords = ref<Map<string | number, any[]>>(new Map());
 
+// 学生请假数据
+const studentLeaves = ref<Map<string | number, any>>(new Map());
+
 // 违纪记录弹窗相关
 const violationDialogVisible = ref(false);
 const currentStudent = ref<any>(null);
 const currentViolation = ref<any>(null);
 const currentViolationIndex = ref(-1);
 
+// 请假流程弹窗相关
+const leaveProcessDialogVisible = ref(false);
+const leaveProcessDialogRef = ref();
+const currentLeaveStudent = ref<any>(null);
+const currentLeaveInfo = ref<any>(null);
+
 // 字典数据
 const { xg_dorm_hygiene_grade, xg_student_attendance_status, xg_dorm_hygiene_check_type, xg_dorm_hygiene_check_level, student_discipline_status } =
   toRefs<any>(
@@ -473,6 +524,12 @@ const formatDate = (dateStr: string) => {
   return new Date(dateStr).toLocaleDateString();
 };
 
+// 格式化日期时间
+const formatDateTime = (dateStr: string) => {
+  if (!dateStr) return '';
+  return new Date(dateStr).toLocaleString();
+};
+
 // 加载违纪类型数据
 const loadDisciplineTypes = async () => {
   try {
@@ -496,9 +553,7 @@ const loadDisciplineTypes = async () => {
   } catch (error) {
     console.error('加载违纪类型数据失败:', error);
     console.error('加载违纪类型数据失败:', error);
-    if (window.ElMessage) {
-      window.ElMessage.error('加载违纪类型数据失败');
-    }
+    ElMessage.error('加载违纪类型数据失败');
     disciplineTypeOptions.value = [];
   }
 };
@@ -514,9 +569,7 @@ const handleDisciplineTypeChange = () => {
         const minScore = parseFloat(selectedType.minScore);
         if (!isNaN(minScore)) {
           hygieneForm.totalScore = minScore;
-          if (window.ElMessage) {
-            window.ElMessage.success(`已自动填入最低扣分:${minScore}分`);
-          }
+          ElMessage.success(`已自动填入最低扣分:${minScore}分`);
         }
       }
     }
@@ -562,17 +615,15 @@ const loadStudentScores = async () => {
       studentScores.value = res.data;
       console.log('学生评分数据加载成功:', res.data.length, '条');
 
-      // 同时加载违纪记录
-      await loadViolationRecords();
+      // 同时加载违纪记录和请假信息
+      await Promise.all([loadViolationRecords(), loadStudentLeaves()]);
     } else {
       studentScores.value = [];
       console.log('学生评分数据为空,响应码:', res.code);
     }
   } catch (error) {
     console.error('加载学生评分数据失败:', error);
-    if (window.ElMessage) {
-      window.ElMessage.error('加载学生评分数据失败');
-    }
+    ElMessage.error('加载学生评分数据失败');
     studentScores.value = [];
   } finally {
     studentsLoading.value = false;
@@ -602,9 +653,39 @@ const loadViolationRecords = async () => {
     }
   } catch (error) {
     console.error('加载违纪记录数据失败:', error);
-    if (window.ElMessage) {
-      window.ElMessage.error('加载违纪记录数据失败');
+    ElMessage.error('加载违纪记录数据失败');
+  }
+};
+
+// 加载学生请假信息
+const loadStudentLeaves = async () => {
+  if (!hygieneForm.checkDate || studentScores.value.length === 0) return;
+
+  try {
+    const studentIds = studentScores.value.map((student) => student.studentId).join(',');
+    const res = await request({
+      url: '/studentLeave/studentLeave/queryByCheckDate',
+      method: 'get',
+      params: {
+        studentIds,
+        checkDate: hygieneForm.checkDate
+      }
+    });
+
+    if (res.code === 200 && res.data) {
+      studentLeaves.value.clear();
+      Object.keys(res.data).forEach((studentId) => {
+        studentLeaves.value.set(Number(studentId), res.data[studentId]);
+        // 如果学生有请假信息,自动设置出勤状态为3(请假)
+        const student = studentScores.value.find((s) => s.studentId === Number(studentId));
+        if (student) {
+          student.attendanceStatus = '3';
+        }
+      });
     }
+  } catch (error) {
+    console.error('加载学生请假信息失败:', error);
+    ElMessage.error('加载学生请假信息失败');
   }
 };
 
@@ -613,6 +694,11 @@ const getStudentViolations = (studentId: string | number) => {
   return violationRecords.value.get(studentId) || [];
 };
 
+// 获取学生的请假信息
+const getStudentLeave = (studentId: string | number) => {
+  return studentLeaves.value.get(studentId) || null;
+};
+
 // 处理附件数据
 const handleAttachmentData = async () => {
   console.log('handleAttachmentData 方法被调用,attachmentIds:', hygieneForm.attachmentIds);
@@ -805,6 +891,31 @@ const handleViolationClose = () => {
   violationDialogVisible.value = false;
 };
 
+// 查看请假流程
+const viewLeaveProcess = async (student: any, leaveInfo: any) => {
+  if (!leaveInfo || !leaveInfo.id) {
+    ElMessage.warning('请假信息不完整,无法查看流程');
+    return;
+  }
+
+  currentLeaveStudent.value = student;
+  currentLeaveInfo.value = leaveInfo;
+  leaveProcessDialogVisible.value = true;
+
+  // 等待弹窗打开后初始化流程数据
+  await nextTick();
+  if (leaveProcessDialogRef.value) {
+    leaveProcessDialogRef.value.init(leaveInfo.id);
+  }
+};
+
+// 请假流程弹窗关闭
+const handleLeaveProcessClose = () => {
+  leaveProcessDialogVisible.value = false;
+  currentLeaveStudent.value = null;
+  currentLeaveInfo.value = null;
+};
+
 // 文件上传相关方法
 const handleBeforeUpload = (file: any) => {
   const isValidType = /\.(jpg|jpeg|png|gif|bmp|webp|pdf|doc|docx|txt|mp4|avi|mov)$/i.test(file.name);
@@ -1154,7 +1265,7 @@ watch(
   display: flex;
   justify-content: space-between;
   align-items: flex-start;
-  margin-bottom: 12px;
+  /* margin-bottom: 12px; */
 }
 
 .student-name-with-gender {
@@ -1177,13 +1288,13 @@ watch(
 }
 
 .student-details {
-  margin-bottom: 16px;
-  font-size: 13px;
+  /* margin-bottom: 16px; */
+  font-size: 12px;
   color: #666;
 }
 
 .student-details p {
-  margin: 4px 0;
+  /* margin: 4px 0; */
 }
 
 .student-score-section {
@@ -1249,6 +1360,59 @@ watch(
   padding: 0 !important;
 }
 
+.leave-section {
+  border-top: 1px solid #f0f0f0;
+  padding-top: 12px;
+  margin-bottom: 12px;
+}
+
+.leave-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.leave-header-actions {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.leave-header span {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.leave-info {
+  background: #f8f9fa;
+  padding: 8px 12px;
+  border-radius: 4px;
+  border: 1px solid #e9ecef;
+}
+
+.leave-item {
+  display: flex;
+  margin-bottom: 4px;
+  font-size: 12px;
+}
+
+.leave-item:last-child {
+  margin-bottom: 0;
+}
+
+.leave-label {
+  color: #666;
+  min-width: 70px;
+  flex-shrink: 0;
+}
+
+.leave-value {
+  color: #303133;
+  flex: 1;
+}
+
 .violation-section {
   border-top: 1px solid #f0f0f0;
   padding-top: 12px;

+ 181 - 9
web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/DormHygieneCheckSingleDialog.vue

@@ -162,8 +162,7 @@
             </div>
 
             <div class="student-details">
-              <p><strong>班级:</strong>{{ student.className }}</p>
-              <p><strong>专业:</strong>{{ student.majorName }}</p>
+              <p><strong>班级:</strong>{{ student.className }} <strong>专业:</strong>{{ student.majorName }}</p>
             </div>
 
             <div class="student-score-section">
@@ -207,6 +206,36 @@
               </el-row>
             </div>
 
+            <!-- 请假信息区域 -->
+            <div class="leave-section" v-if="getStudentLeave(student.studentId)">
+              <div class="leave-header">
+                <span>请假信息</span>
+                <div class="leave-header-actions">
+                  <el-button size="small" @click="viewLeaveProcess(student, getStudentLeave(student.studentId))" title="查看请假流程">
+                    查看流程
+                  </el-button>
+                  <el-tag type="warning" size="small">请假中</el-tag>
+                </div>
+              </div>
+              <div class="leave-info">
+                <div class="leave-item">
+                  <span class="leave-label">发起时间:</span>
+                  <span class="leave-value">{{ formatDateTime(getStudentLeave(student.studentId)?.createTime) }}</span>
+                </div>
+                <div class="leave-item">
+                  <span class="leave-label">请假时间:</span>
+                  <span class="leave-value"
+                    >{{ formatDateTime(getStudentLeave(student.studentId)?.startTime) }} 至
+                    {{ formatDateTime(getStudentLeave(student.studentId)?.endTime) }}</span
+                  >
+                </div>
+                <div class="leave-item">
+                  <span class="leave-label">请假类型:</span>
+                  <span class="leave-value">{{ getStudentLeave(student.studentId)?.leaveTypeName }}</span>
+                </div>
+              </div>
+            </div>
+
             <!-- 违纪录入区域 -->
             <div class="violation-section">
               <div class="violation-header">
@@ -258,18 +287,30 @@
       @confirm="handleViolationConfirm"
       @close="handleViolationClose"
     />
+
+    <!-- 请假流程查看弹窗 -->
+    <LeaveProcessDialog
+      ref="leaveProcessDialogRef"
+      :visible="leaveProcessDialogVisible"
+      :student-info="currentLeaveStudent"
+      :leave-info="currentLeaveInfo"
+      @update:visible="leaveProcessDialogVisible = $event"
+      @close="handleLeaveProcessClose"
+    />
   </el-dialog>
 </template>
 
 <script setup name="DormHygieneCheckSingleDialog" lang="ts">
-import { ref, reactive, computed, watch, toRefs, getCurrentInstance, type ComponentInternalInstance } from 'vue';
+import { ref, reactive, computed, watch, toRefs, getCurrentInstance, nextTick, type ComponentInternalInstance } from 'vue';
 import { ElMessage, ElMessageBox } from 'element-plus';
-import { Plus, InfoFilled, UploadFilled, Document, Loading, Download, Delete } from '@element-plus/icons-vue';
+import { Plus, InfoFilled, UploadFilled, Document, Loading, Download, Delete, View } from '@element-plus/icons-vue';
 import { getToken } from '@/utils/auth';
 import ImagePreview from '@/components/ImagePreview/index.vue';
 import request from '@/utils/request';
-// @ts-ignore
+// @ts-expect-error - Component import without type declarations
 import ViolationRecordDialog from './components/ViolationRecordDialog.vue';
+// @ts-expect-error - Component import without type declarations
+import LeaveProcessDialog from './components/LeaveProcessDialog.vue';
 
 // 组件实例
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -334,6 +375,7 @@ const hygieneRules = reactive({
 // 学生相关数据
 const roomStudents = ref<any[]>([]);
 const violationRecords = ref<Map<string | number, any[]>>(new Map());
+const studentLeaves = ref<Map<string | number, any>>(new Map());
 
 // 违纪记录弹窗相关
 const violationDialogVisible = ref(false);
@@ -341,6 +383,12 @@ const currentStudent = ref<any>(null);
 const currentViolation = ref<any>(null);
 const currentViolationIndex = ref(-1);
 
+// 请假流程弹窗相关
+const leaveProcessDialogVisible = ref(false);
+const leaveProcessDialogRef = ref();
+const currentLeaveStudent = ref<any>(null);
+const currentLeaveInfo = ref<any>(null);
+
 // 字典数据
 const { xg_dorm_hygiene_grade, xg_student_attendance_status } = toRefs<any>(
   (proxy as any)?.useDict('xg_dorm_hygiene_grade', 'xg_student_attendance_status')
@@ -439,6 +487,9 @@ const loadRoomStudents = async () => {
         attendance_status: '1',
         student_grade: 'A'
       }));
+
+      // 加载学生请假信息
+      await loadStudentLeaves();
     } else {
       roomStudents.value = [];
     }
@@ -451,6 +502,38 @@ const loadRoomStudents = async () => {
   }
 };
 
+// 加载学生请假信息
+const loadStudentLeaves = async () => {
+  if (!props.checkInfo?.checkDate || roomStudents.value.length === 0) return;
+
+  try {
+    const studentIds = roomStudents.value.map((student) => student.studentId).join(',');
+    const res = await request({
+      url: '/studentLeave/studentLeave/queryByCheckDate',
+      method: 'get',
+      params: {
+        studentIds,
+        checkDate: props.checkInfo.checkDate
+      }
+    });
+
+    if (res.code === 200 && res.data) {
+      studentLeaves.value.clear();
+      Object.keys(res.data).forEach((studentId) => {
+        studentLeaves.value.set(Number(studentId), res.data[studentId]);
+        // 如果学生有请假信息,自动设置出勤状态为3(请假)
+        const student = roomStudents.value.find((s) => s.studentId === Number(studentId));
+        if (student) {
+          student.attendance_status = '3';
+        }
+      });
+    }
+  } catch (error) {
+    console.error('加载学生请假信息失败:', error);
+    ElMessage.error('加载学生请假信息失败');
+  }
+};
+
 // 获取违纪类型说明
 const selectedDisciplineTypeRemark = computed(() => {
   if (hygieneForm.disciplineTypeId) {
@@ -465,6 +548,17 @@ const getStudentViolations = (studentId: string | number) => {
   return violationRecords.value.get(studentId) || [];
 };
 
+// 获取学生的请假信息
+const getStudentLeave = (studentId: string | number) => {
+  return studentLeaves.value.get(studentId) || null;
+};
+
+// 格式化日期时间
+const formatDateTime = (dateStr: string) => {
+  if (!dateStr) return '';
+  return new Date(dateStr).toLocaleString();
+};
+
 // 添加违纪记录
 const addViolationRecord = (student: any) => {
   currentStudent.value = student;
@@ -529,6 +623,31 @@ const handleViolationClose = () => {
   violationDialogVisible.value = false;
 };
 
+// 查看请假流程
+const viewLeaveProcess = async (student: any, leaveInfo: any) => {
+  if (!leaveInfo || !leaveInfo.id) {
+    ElMessage.warning('请假信息不完整,无法查看流程');
+    return;
+  }
+
+  currentLeaveStudent.value = student;
+  currentLeaveInfo.value = leaveInfo;
+  leaveProcessDialogVisible.value = true;
+
+  // 等待弹窗打开后初始化流程数据
+  await nextTick();
+  if (leaveProcessDialogRef.value) {
+    leaveProcessDialogRef.value.init(leaveInfo.id);
+  }
+};
+
+// 请假流程弹窗关闭
+const handleLeaveProcessClose = () => {
+  leaveProcessDialogVisible.value = false;
+  currentLeaveStudent.value = null;
+  currentLeaveInfo.value = null;
+};
+
 // 处理宿舍总分变化
 const handleTotalScoreChange = () => {
   // 移除联动逻辑,仅做数值验证
@@ -949,7 +1068,7 @@ watch(
   display: flex;
   justify-content: space-between;
   align-items: flex-start;
-  margin-bottom: 12px;
+  /* margin-bottom: 12px; */
 }
 
 .student-name-with-gender {
@@ -972,13 +1091,13 @@ watch(
 }
 
 .student-details {
-  margin-bottom: 16px;
-  font-size: 13px;
+  /* margin-bottom: 16px; */
+  font-size: 12px;
   color: #666;
 }
 
 .student-details p {
-  margin: 4px 0;
+  /* margin: 4px 0; */
 }
 
 .student-score-section {
@@ -1119,6 +1238,59 @@ watch(
   max-height: 32px !important;
 }
 
+.leave-section {
+  border-top: 1px solid #f0f0f0;
+  padding-top: 12px;
+  margin-bottom: 12px;
+}
+
+.leave-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.leave-header-actions {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.leave-header span {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.leave-info {
+  background: #f8f9fa;
+  padding: 8px 12px;
+  border-radius: 4px;
+  border: 1px solid #e9ecef;
+}
+
+.leave-item {
+  display: flex;
+  margin-bottom: 4px;
+  font-size: 12px;
+}
+
+.leave-item:last-child {
+  margin-bottom: 0;
+}
+
+.leave-label {
+  color: #666;
+  min-width: 70px;
+  flex-shrink: 0;
+}
+
+.leave-value {
+  color: #303133;
+  flex: 1;
+}
+
 .violation-section {
   border-top: 1px solid #f0f0f0;
   padding-top: 12px;

+ 322 - 0
web/src/views/xg/xgss/dormHygieneCheck/dormHygieneCheck/components/LeaveProcessDialog.vue

@@ -0,0 +1,322 @@
+<template>
+  <el-dialog v-model="dialogVisible" title="请假流程信息" width="800px" append-to-body @close="handleClose">
+    <div v-loading="loading" class="leave-process-container">
+      <div v-if="studentInfo" class="student-info-header">
+        <div class="student-basic">
+          <h4>{{ studentInfo.studentName }}</h4>
+          <span class="student-no">{{ studentInfo.studentNo }}</span>
+        </div>
+        <div class="leave-basic">
+          <el-tag type="warning" size="small">请假中</el-tag>
+        </div>
+      </div>
+
+      <div v-if="leaveInfo" class="leave-detail-info">
+        <div class="leave-item">
+          <span class="leave-label">请假类型:</span>
+          <span class="leave-value">{{ leaveInfo.leaveTypeName }}</span>
+        </div>
+        <div class="leave-item">
+          <span class="leave-label">请假时间:</span>
+          <span class="leave-value"> {{ formatDateTime(leaveInfo.startTime) }} 至 {{ formatDateTime(leaveInfo.endTime) }} </span>
+        </div>
+        <div class="leave-item">
+          <span class="leave-label">请假原因:</span>
+          <span class="leave-value">{{ leaveInfo.reason || '无' }}</span>
+        </div>
+      </div>
+
+      <div class="approval-records">
+        <h5>审批记录</h5>
+        <el-table :data="approvalList" border style="width: 100%">
+          <el-table-column type="index" label="序号" align="center" width="60" />
+          <el-table-column prop="nodeName" label="任务名称" align="center" />
+          <el-table-column prop="approveName" label="办理人" align="center">
+            <template #default="scope">
+              <template v-if="scope.row.approveName">
+                <el-tag v-for="(item, index) in scope.row.approveName.split(',')" :key="index" type="success" size="small">
+                  {{ item }}
+                </el-tag>
+              </template>
+              <template v-else>
+                <el-tag type="info" size="small">无</el-tag>
+              </template>
+            </template>
+          </el-table-column>
+          <el-table-column prop="approverPhone" label="办理人电话" width="130" align="center">
+            <template #default="scope">
+              <span v-if="scope.row.approverPhone && scope.row.approverPhone !== '未录入'">{{ scope.row.approverPhone }}</span>
+              <el-tag v-else type="info" size="small">未录入</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="flowStatus" label="状态" width="100" align="center">
+            <template #default="scope">
+              <el-tag :type="getStatusType(scope.row.flowStatus)" size="small">
+                {{ getStatusText(scope.row.flowStatus) }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="message" label="审批意见" align="center">
+            <template #default="scope">
+              <span>{{ scope.row.message || '无' }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="createTime" label="开始时间" width="160" align="center" />
+          <el-table-column prop="updateTime" label="结束时间" width="160" align="center">
+            <template #default="scope">
+              <span>{{ scope.row.updateTime || '进行中' }}</span>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="handleClose">关 闭</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup name="LeaveProcessDialog" lang="ts">
+import { ref, computed } from 'vue';
+import request from '@/utils/request';
+
+// 组件属性
+interface Props {
+  visible: boolean;
+  studentInfo: any;
+  leaveInfo: any;
+}
+
+// 组件事件
+interface Emits {
+  (e: 'update:visible', value: boolean): void;
+  (e: 'close'): void;
+}
+
+const props = defineProps<Props>();
+const emit = defineEmits<Emits>();
+
+// 响应式数据
+const loading = ref(false);
+const approvalList = ref<any[]>([]);
+
+// 计算属性
+const dialogVisible = computed({
+  get: () => props.visible,
+  set: (value) => emit('update:visible', value)
+});
+
+// 格式化日期时间
+const formatDateTime = (dateStr: string) => {
+  if (!dateStr) return '';
+  return new Date(dateStr).toLocaleString();
+};
+
+// 获取状态类型
+const getStatusType = (status: string) => {
+  switch (status) {
+    case 'pass':
+      return 'success';
+    case 'waiting':
+      return 'warning';
+    case 'reject':
+      return 'danger';
+    case 'draft':
+      return 'info';
+    default:
+      return 'info';
+  }
+};
+
+// 获取状态文本
+const getStatusText = (status: string) => {
+  switch (status) {
+    case 'pass':
+      return '通过';
+    case 'waiting':
+      return '待审批';
+    case 'reject':
+      return '拒绝';
+    case 'draft':
+      return '草稿';
+    default:
+      return status || '未知';
+  }
+};
+
+// 获取用户电话信息
+const getUserPhones = async (userIds: string[]) => {
+  if (!userIds || userIds.length === 0) {
+    return {};
+  }
+
+  try {
+    // 过滤掉空值和重复值
+    const validUserIds = [...new Set(userIds.filter((id) => id && id.trim()))];
+    if (validUserIds.length === 0) {
+      return {};
+    }
+
+    const response = await request({
+      url: `/system/user/optionselect?userIds=${validUserIds.join(',')}`,
+      method: 'get'
+    });
+
+    if (response.code === 200 && response.data) {
+      const phoneMap: Record<string, string> = {};
+      response.data.forEach((user: any) => {
+        if (user.userId) {
+          // 如果用户有手机号则使用,否则标记为"未录入"
+          phoneMap[user.userId.toString()] = user.phonenumber || '未录入';
+        }
+      });
+      return phoneMap;
+    }
+  } catch (error) {
+    console.error('获取用户电话失败:', error);
+  }
+  return {};
+};
+
+// 加载审批记录
+const loadApprovalRecords = async (businessId: string | number) => {
+  if (!businessId) {
+    console.warn('businessId为空,无法加载审批记录');
+    return;
+  }
+
+  loading.value = true;
+  try {
+    const response = await request({
+      url: `/workflow/instance/flowHisTaskList/${businessId}`,
+      method: 'get'
+    });
+
+    if (response.code === 200 && response.data?.list) {
+      approvalList.value = response.data.list;
+      console.log('审批记录加载成功:', approvalList.value.length, '条');
+
+      // 提取所有办理人ID
+      const approverIds: string[] = [];
+      approvalList.value.forEach((item: any) => {
+        if (item.approver) {
+          // approver可能包含多个用户ID,用逗号分隔
+          const ids = item.approver.split(',').map((id: string) => id.trim());
+          approverIds.push(...ids);
+        }
+      });
+
+      // 获取办理人电话信息
+      if (approverIds.length > 0) {
+        const phoneMap = await getUserPhones(approverIds);
+
+        // 为每条审批记录设置办理人电话
+        approvalList.value.forEach((item: any) => {
+          if (item.approver) {
+            const ids = item.approver.split(',').map((id: string) => id.trim());
+            const phones = ids.map((id: string) => phoneMap[id] || '未录入');
+            item.approverPhone = phones.length > 0 ? phones.join(', ') : '未录入';
+          } else {
+            item.approverPhone = '未录入';
+          }
+        });
+      }
+    } else {
+      approvalList.value = [];
+      console.warn('审批记录数据为空');
+    }
+  } catch (error) {
+    console.error('加载审批记录失败:', error);
+    approvalList.value = [];
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 关闭弹窗
+const handleClose = () => {
+  emit('close');
+  // 清空数据
+  approvalList.value = [];
+};
+
+// 对外暴露方法
+const init = async (businessId: string | number) => {
+  console.log('初始化请假流程对话框,businessId:', businessId);
+  await loadApprovalRecords(businessId);
+};
+
+defineExpose({
+  init
+});
+</script>
+
+<style scoped>
+.leave-process-container {
+  min-height: 400px;
+}
+
+.student-info-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px;
+  background: #f8f9fa;
+  border-radius: 6px;
+  margin-bottom: 20px;
+}
+
+.student-basic h4 {
+  margin: 0 0 4px 0;
+  color: #303133;
+  font-size: 16px;
+}
+
+.student-no {
+  color: #666;
+  font-size: 14px;
+}
+
+.leave-detail-info {
+  background: #fff;
+  border: 1px solid #e4e7ed;
+  border-radius: 6px;
+  padding: 16px;
+  margin-bottom: 20px;
+}
+
+.leave-item {
+  display: flex;
+  margin-bottom: 8px;
+  font-size: 14px;
+}
+
+.leave-item:last-child {
+  margin-bottom: 0;
+}
+
+.leave-label {
+  color: #666;
+  min-width: 80px;
+  flex-shrink: 0;
+}
+
+.leave-value {
+  color: #303133;
+  flex: 1;
+}
+
+.approval-records h5 {
+  margin: 0 0 16px 0;
+  color: #303133;
+  font-size: 16px;
+  font-weight: 600;
+}
+
+.dialog-footer {
+  text-align: center;
+}
+</style>