提交代码

This commit is contained in:
zlwu committed 2025-09-06 23:36:10 +08:00
1 parent f35359a84e
commit 0f199aa50d
7 files changed
+763 -32

No files matched your search

@@ -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));
@@ -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());
}
}
}
@@ -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);
}
@@ -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 // 保留第一条(最新的)
));
}
}
@@ -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;
@@ -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;
@@ -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>