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

+ 87 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/controller/XgStudentDynamicsController.java

@@ -0,0 +1,87 @@
+package org.dromara.xg.controller;
+
+import lombok.RequiredArgsConstructor;
+import jakarta.validation.constraints.NotNull;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.validation.annotation.Validated;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.xg.domain.vo.XgDormApplicationVo;
+import org.dromara.xg.domain.vo.XgHolidayStayVo;
+import org.dromara.xg.domain.vo.XgStudentLeaveVo;
+import org.dromara.xg.domain.bo.XgDormApplicationBo;
+import org.dromara.xg.domain.bo.XgHolidayStayBo;
+import org.dromara.xg.domain.bo.XgStudentLeaveBo;
+import org.dromara.xg.service.IXgStudentDynamicsService;
+
+/**
+ * 学生动向信息管理(宿管员)
+ *
+ * @author zlwu
+ * @date 2025-01-15
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/studentDynamics")
+public class XgStudentDynamicsController extends BaseController {
+
+    private final IXgStudentDynamicsService xgStudentDynamicsService;
+
+    /**
+     * 查询宿管员权限内的学生调宿退宿申请列表
+     */
+    @SaCheckPermission("studentDynamics:dormApplication:list")
+    @GetMapping("/dormApplicationList")
+    public TableDataInfo<XgDormApplicationVo> getDormApplicationList(XgDormApplicationBo bo, PageQuery pageQuery) {
+        return xgStudentDynamicsService.queryDormApplicationPageList(bo, pageQuery);
+    }
+
+    /**
+     * 查询宿管员权限内的学生假期留宿信息列表
+     */
+    @SaCheckPermission("studentDynamics:holidayStay:list")
+    @GetMapping("/holidayStayList")
+    public TableDataInfo<XgHolidayStayVo> getHolidayStayList(XgHolidayStayBo bo, PageQuery pageQuery) {
+        return xgStudentDynamicsService.queryHolidayStayPageList(bo, pageQuery);
+    }
+
+    /**
+     * 查询宿管员权限内的学生请假信息列表
+     */
+    @SaCheckPermission("studentDynamics:studentLeave:list")
+    @GetMapping("/studentLeaveList")
+    public TableDataInfo<XgStudentLeaveVo> getStudentLeaveList(XgStudentLeaveBo bo, PageQuery pageQuery) {
+        return xgStudentDynamicsService.queryStudentLeavePageList(bo, pageQuery);
+    }
+
+    /**
+     * 获取调宿退宿申请详细信息
+     */
+    @SaCheckPermission("studentDynamics:dormApplication:query")
+    @GetMapping("/dormApplication/{id}")
+    public R<XgDormApplicationVo> getDormApplicationInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
+        return R.ok(xgStudentDynamicsService.queryDormApplicationById(id));
+    }
+
+    /**
+     * 获取假期留宿详细信息
+     */
+    @SaCheckPermission("studentDynamics:holidayStay:query")
+    @GetMapping("/holidayStay/{id}")
+    public R<XgHolidayStayVo> getHolidayStayInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
+        return R.ok(xgStudentDynamicsService.queryHolidayStayById(id));
+    }
+
+    /**
+     * 获取学生请假详细信息
+     */
+    @SaCheckPermission("studentDynamics:studentLeave:query")
+    @GetMapping("/studentLeave/{id}")
+    public R<XgStudentLeaveVo> getStudentLeaveInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
+        return R.ok(xgStudentDynamicsService.queryStudentLeaveById(id));
+    }
+}

+ 85 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/mapper/XgStudentDynamicsMapper.java

@@ -0,0 +1,85 @@
+package org.dromara.xg.mapper;
+
+import org.apache.ibatis.annotations.Param;
+import java.util.List;
+
+/**
+ * 学生动向信息管理(宿管员)Mapper接口
+ *
+ * @author zlwu
+ * @date 2025-01-15
+ */
+public interface XgStudentDynamicsMapper {
+
+        /**
+         * 检查用户是否有宿舍管理权限
+         *
+         * @param userId 用户ID
+         * @return 管理员类型列表 (1全校管理员 2校区管理员 3宿舍区管理员 4宿舍楼管理员)
+         */
+        List<String> checkUserDormManagerType(@Param("userId") Long userId);
+
+        /**
+         * 获取全校管理员权限的所有楼栋ID列表
+         *
+         * @param userId 用户ID
+         * @return 楼栋ID列表
+         */
+        List<Long> getBuildingIdsBySchoolAdmin(@Param("userId") Long userId);
+
+        /**
+         * 获取校区管理员权限的楼栋ID列表
+         *
+         * @param userId 用户ID
+         * @return 楼栋ID列表
+         */
+        List<Long> getBuildingIdsByCampusAdmin(@Param("userId") Long userId);
+
+        /**
+         * 获取宿舍区管理员权限的楼栋ID列表
+         *
+         * @param userId 用户ID
+         * @return 楼栋ID列表
+         */
+        List<Long> getBuildingIdsByAreaAdmin(@Param("userId") Long userId);
+
+        /**
+         * 获取宿舍楼管理员权限的楼栋ID列表
+         *
+         * @param userId 用户ID
+         * @return 楼栋ID列表
+         */
+        List<Long> getBuildingIdsByBuildingAdmin(@Param("userId") Long userId);
+
+        /**
+         * 获取全校管理员权限的所有学生ID列表
+         *
+         * @param userId 用户ID
+         * @return 学生ID列表
+         */
+        List<Long> getStudentIdsBySchoolAdmin(@Param("userId") Long userId);
+
+        /**
+         * 获取校区管理员权限的学生ID列表
+         *
+         * @param userId 用户ID
+         * @return 学生ID列表
+         */
+        List<Long> getStudentIdsByCampusAdmin(@Param("userId") Long userId);
+
+        /**
+         * 获取宿舍区管理员权限的学生ID列表
+         *
+         * @param userId 用户ID
+         * @return 学生ID列表
+         */
+        List<Long> getStudentIdsByAreaAdmin(@Param("userId") Long userId);
+
+        /**
+         * 获取宿舍楼管理员权限的学生ID列表
+         *
+         * @param userId 用户ID
+         * @return 学生ID列表
+         */
+        List<Long> getStudentIdsByBuildingAdmin(@Param("userId") Long userId);
+}

+ 49 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/java/org/dromara/xg/service/IXgStudentDynamicsService.java

@@ -0,0 +1,49 @@
+package org.dromara.xg.service;
+
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.xg.domain.vo.XgDormApplicationVo;
+import org.dromara.xg.domain.vo.XgHolidayStayVo;
+import org.dromara.xg.domain.vo.XgStudentLeaveVo;
+import org.dromara.xg.domain.bo.XgDormApplicationBo;
+import org.dromara.xg.domain.bo.XgHolidayStayBo;
+import org.dromara.xg.domain.bo.XgStudentLeaveBo;
+
+/**
+ * 学生动向信息管理(宿管员)Service接口
+ *
+ * @author zlwu
+ * @date 2025-01-15
+ */
+public interface IXgStudentDynamicsService {
+
+    /**
+     * 查询宿管员权限内的学生调宿退宿申请列表
+     */
+    TableDataInfo<XgDormApplicationVo> queryDormApplicationPageList(XgDormApplicationBo bo, PageQuery pageQuery);
+
+    /**
+     * 查询宿管员权限内的学生假期留宿信息列表
+     */
+    TableDataInfo<XgHolidayStayVo> queryHolidayStayPageList(XgHolidayStayBo bo, PageQuery pageQuery);
+
+    /**
+     * 查询宿管员权限内的学生请假信息列表
+     */
+    TableDataInfo<XgStudentLeaveVo> queryStudentLeavePageList(XgStudentLeaveBo bo, PageQuery pageQuery);
+
+    /**
+     * 根据主键查询调宿退宿申请详情
+     */
+    XgDormApplicationVo queryDormApplicationById(Long id);
+
+    /**
+     * 根据主键查询假期留宿详情
+     */
+    XgHolidayStayVo queryHolidayStayById(Long id);
+
+    /**
+     * 根据主键查询学生请假详情
+     */
+    XgStudentLeaveVo queryStudentLeaveById(Long id);
+}

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

@@ -0,0 +1,329 @@
+package org.dromara.xg.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.xg.domain.vo.XgDormApplicationVo;
+import org.dromara.xg.domain.vo.XgHolidayStayVo;
+import org.dromara.xg.domain.vo.XgStudentLeaveVo;
+import org.dromara.xg.domain.bo.XgDormApplicationBo;
+import org.dromara.xg.domain.bo.XgHolidayStayBo;
+import org.dromara.xg.domain.bo.XgStudentLeaveBo;
+import org.dromara.xg.domain.XgDormApplication;
+import org.dromara.xg.domain.XgHolidayStay;
+import org.dromara.xg.domain.XgStudentLeave;
+import org.dromara.xg.mapper.XgDormApplicationMapper;
+import org.dromara.xg.mapper.XgHolidayStayMapper;
+import org.dromara.xg.mapper.XgStudentLeaveMapper;
+import org.dromara.xg.mapper.XgStudentDynamicsMapper;
+import org.dromara.xg.service.IXgStudentDynamicsService;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 学生动向信息管理(宿管员)Service实现
+ *
+ * @author zlwu
+ * @date 2025-01-15
+ */
+@RequiredArgsConstructor
+@Service
+public class XgStudentDynamicsServiceImpl implements IXgStudentDynamicsService {
+
+    private final XgDormApplicationMapper dormApplicationMapper;
+    private final XgHolidayStayMapper holidayStayMapper;
+    private final XgStudentLeaveMapper studentLeaveMapper;
+    private final XgStudentDynamicsMapper studentDynamicsMapper;
+
+    /**
+     * 查询宿管员权限内的学生调宿退宿申请列表
+     */
+    @Override
+    public TableDataInfo<XgDormApplicationVo> queryDormApplicationPageList(XgDormApplicationBo bo,
+            PageQuery pageQuery) {
+        // 获取当前登录用户ID
+        Long currentUserId = LoginHelper.getUserId();
+
+        // 构建查询条件
+        LambdaQueryWrapper<XgDormApplication> lqw = buildDormApplicationQueryWrapper(bo, currentUserId);
+
+        // 分页查询
+        Page<XgDormApplicationVo> result = dormApplicationMapper.selectVoPage(pageQuery.build(), lqw);
+        return TableDataInfo.build(result);
+    }
+
+    /**
+     * 查询宿管员权限内的学生假期留宿信息列表
+     */
+    @Override
+    public TableDataInfo<XgHolidayStayVo> queryHolidayStayPageList(XgHolidayStayBo bo, PageQuery pageQuery) {
+        // 获取当前登录用户ID
+        Long currentUserId = LoginHelper.getUserId();
+
+        // 构建查询条件
+        LambdaQueryWrapper<XgHolidayStay> lqw = buildHolidayStayQueryWrapper(bo, currentUserId);
+
+        // 分页查询
+        Page<XgHolidayStayVo> result = holidayStayMapper.selectVoPage(pageQuery.build(), lqw);
+        return TableDataInfo.build(result);
+    }
+
+    /**
+     * 查询宿管员权限内的学生请假信息列表
+     */
+    @Override
+    public TableDataInfo<XgStudentLeaveVo> queryStudentLeavePageList(XgStudentLeaveBo bo, PageQuery pageQuery) {
+        // 获取当前登录用户ID
+        Long currentUserId = LoginHelper.getUserId();
+
+        // 构建查询条件
+        LambdaQueryWrapper<XgStudentLeave> lqw = buildStudentLeaveQueryWrapper(bo, currentUserId);
+
+        // 分页查询
+        Page<XgStudentLeaveVo> result = studentLeaveMapper.selectVoPage(pageQuery.build(), lqw);
+        return TableDataInfo.build(result);
+    }
+
+    /**
+     * 根据主键查询调宿退宿申请详情
+     */
+    @Override
+    public XgDormApplicationVo queryDormApplicationById(Long id) {
+        return dormApplicationMapper.selectVoById(id);
+    }
+
+    /**
+     * 根据主键查询假期留宿详情
+     */
+    @Override
+    public XgHolidayStayVo queryHolidayStayById(Long id) {
+        return holidayStayMapper.selectVoById(id);
+    }
+
+    /**
+     * 根据主键查询学生请假详情
+     */
+    @Override
+    public XgStudentLeaveVo queryStudentLeaveById(Long id) {
+        return studentLeaveMapper.selectVoById(id);
+    }
+
+    /**
+     * 构建调宿退宿申请查询条件(带宿管员权限)
+     */
+    private LambdaQueryWrapper<XgDormApplication> buildDormApplicationQueryWrapper(XgDormApplicationBo bo,
+            Long currentUserId) {
+        LambdaQueryWrapper<XgDormApplication> lqw = new LambdaQueryWrapper<>();
+
+        // 获取宿管员管理的楼栋ID列表
+        List<Long> managedBuildingIds = getManagedBuildingIds(currentUserId);
+
+        if (managedBuildingIds.isEmpty()) {
+            // 如果没有管理权限,返回空结果
+            lqw.eq(XgDormApplication::getId, -1);
+            return lqw;
+        }
+
+        // 只查询宿管员权限内楼栋的学生申请
+        lqw.in(XgDormApplication::getCurrentBuildingId, managedBuildingIds);
+
+        // 添加其他查询条件
+        lqw.like(bo.getStudentName() != null, XgDormApplication::getStudentName, bo.getStudentName());
+        lqw.like(bo.getStudentNo() != null, XgDormApplication::getStudentNo, bo.getStudentNo());
+        lqw.like(bo.getCollegeName() != null, XgDormApplication::getCollegeName, bo.getCollegeName());
+        lqw.like(bo.getMajorName() != null, XgDormApplication::getMajorName, bo.getMajorName());
+        lqw.like(bo.getClassName() != null, XgDormApplication::getClassName, bo.getClassName());
+        lqw.eq(bo.getApplicationType() != null, XgDormApplication::getApplicationType, bo.getApplicationType());
+        lqw.eq(bo.getApplicationStatus() != null, XgDormApplication::getApplicationStatus, bo.getApplicationStatus());
+        lqw.ge(bo.getApplyTime() != null, XgDormApplication::getApplyTime, bo.getApplyTime());
+
+        // 默认按创建时间倒序
+        lqw.orderByDesc(XgDormApplication::getCreateTime);
+
+        return lqw;
+    }
+
+    /**
+     * 构建假期留宿查询条件(带宿管员权限)
+     */
+    private LambdaQueryWrapper<XgHolidayStay> buildHolidayStayQueryWrapper(XgHolidayStayBo bo, Long currentUserId) {
+        LambdaQueryWrapper<XgHolidayStay> lqw = new LambdaQueryWrapper<>();
+
+        // 获取宿管员管理的楼栋ID列表
+        List<Long> managedBuildingIds = getManagedBuildingIds(currentUserId);
+
+        if (managedBuildingIds.isEmpty()) {
+            // 如果没有管理权限,返回空结果
+            lqw.eq(XgHolidayStay::getId, -1);
+            return lqw;
+        }
+
+        // 只查询宿管员权限内楼栋的学生留宿
+        lqw.in(XgHolidayStay::getBuildingId, managedBuildingIds);
+
+        // 添加其他查询条件
+        lqw.like(bo.getStudentName() != null, XgHolidayStay::getStudentName, bo.getStudentName());
+        lqw.like(bo.getStudentNo() != null, XgHolidayStay::getStudentNo, bo.getStudentNo());
+        lqw.like(bo.getCollegeName() != null, XgHolidayStay::getCollegeName, bo.getCollegeName());
+        lqw.like(bo.getMajorName() != null, XgHolidayStay::getMajorName, bo.getMajorName());
+        lqw.like(bo.getClassName() != null, XgHolidayStay::getClassName, bo.getClassName());
+        lqw.eq(bo.getHolidayType() != null, XgHolidayStay::getHolidayType, bo.getHolidayType());
+        lqw.ge(bo.getApplyStartDate() != null, XgHolidayStay::getApplyStartDate, bo.getApplyStartDate());
+        lqw.le(bo.getApplyEndDate() != null, XgHolidayStay::getApplyEndDate, bo.getApplyEndDate());
+
+        // 默认按创建时间倒序
+        lqw.orderByDesc(XgHolidayStay::getCreateTime);
+
+        return lqw;
+    }
+
+    /**
+     * 构建学生请假查询条件(带宿管员权限)
+     */
+    private LambdaQueryWrapper<XgStudentLeave> buildStudentLeaveQueryWrapper(XgStudentLeaveBo bo, Long currentUserId) {
+        LambdaQueryWrapper<XgStudentLeave> lqw = new LambdaQueryWrapper<>();
+
+        // 获取宿管员权限内的学生ID列表
+        List<Long> managedStudentIds = getManagedStudentIds(currentUserId);
+
+        if (managedStudentIds.isEmpty()) {
+            // 如果没有管理权限,返回空结果
+            lqw.eq(XgStudentLeave::getId, -1);
+            return lqw;
+        }
+
+        // 只查询宿管员权限内的学生请假
+        lqw.in(XgStudentLeave::getStudentId, managedStudentIds);
+
+        // 添加其他查询条件
+        lqw.like(bo.getStudentName() != null, XgStudentLeave::getStudentName, bo.getStudentName());
+        lqw.like(bo.getStudentNo() != null, XgStudentLeave::getStudentNo, bo.getStudentNo());
+        lqw.like(bo.getCollegeName() != null, XgStudentLeave::getCollegeName, bo.getCollegeName());
+        lqw.like(bo.getMajorName() != null, XgStudentLeave::getMajorName, bo.getMajorName());
+        lqw.like(bo.getClassName() != null, XgStudentLeave::getClassName, bo.getClassName());
+        lqw.like(bo.getLeaveTypeName() != null, XgStudentLeave::getLeaveTypeName, bo.getLeaveTypeName());
+        lqw.eq(bo.getApprovalStatus() != null, XgStudentLeave::getApprovalStatus, bo.getApprovalStatus());
+        lqw.ge(bo.getStartTime() != null, XgStudentLeave::getStartTime, bo.getStartTime());
+        lqw.le(bo.getEndTime() != null, XgStudentLeave::getEndTime, bo.getEndTime());
+
+        // 默认按创建时间倒序
+        lqw.orderByDesc(XgStudentLeave::getCreateTime);
+
+        return lqw;
+    }
+
+    /**
+     * 获取宿管员管理的楼栋ID列表(支持多级权限合并)
+     */
+    private List<Long> getManagedBuildingIds(Long userId) {
+        // 查询用户管理权限类型
+        List<String> managerTypes = studentDynamicsMapper.checkUserDormManagerType(userId);
+
+        if (managerTypes == null || managerTypes.isEmpty()) {
+            // 没有宿舍管理权限,返回空列表
+            return new ArrayList<>();
+        }
+
+        // 存储所有权限的楼栋ID,使用Set去重
+        Set<Long> buildingIdSet = new HashSet<>();
+
+        // 按照权限分别查询并合并结果(支持用户拥有多种角色的情况)
+        if (managerTypes.contains("1")) {
+            // 全校管理员,获取所有楼栋
+            List<Long> allBuildings = studentDynamicsMapper.getBuildingIdsBySchoolAdmin(userId);
+            if (allBuildings != null) {
+                buildingIdSet.addAll(allBuildings);
+            }
+            // 全校管理员可以查看所有楼栋,直接返回
+            return new ArrayList<>(buildingIdSet);
+        }
+
+        // 对于非全校管理员,合并多种权限的结果
+        if (managerTypes.contains("2")) {
+            // 校区管理员,获取管理的校区下的楼栋
+            List<Long> campusBuildings = studentDynamicsMapper.getBuildingIdsByCampusAdmin(userId);
+            if (campusBuildings != null) {
+                buildingIdSet.addAll(campusBuildings);
+            }
+        }
+
+        if (managerTypes.contains("3")) {
+            // 宿舍区管理员,获取管理的宿舍区下的楼栋
+            List<Long> areaBuildings = studentDynamicsMapper.getBuildingIdsByAreaAdmin(userId);
+            if (areaBuildings != null) {
+                buildingIdSet.addAll(areaBuildings);
+            }
+        }
+
+        if (managerTypes.contains("4")) {
+            // 宿舍楼管理员,获取管理的楼栋
+            List<Long> directBuildings = studentDynamicsMapper.getBuildingIdsByBuildingAdmin(userId);
+            if (directBuildings != null) {
+                buildingIdSet.addAll(directBuildings);
+            }
+        }
+
+        return new ArrayList<>(buildingIdSet);
+    }
+
+    /**
+     * 获取宿管员权限内的学生ID列表(支持多级权限合并)
+     */
+    private List<Long> getManagedStudentIds(Long userId) {
+        // 查询用户管理权限类型
+        List<String> managerTypes = studentDynamicsMapper.checkUserDormManagerType(userId);
+
+        if (managerTypes == null || managerTypes.isEmpty()) {
+            // 没有宿舍管理权限,返回空列表
+            return new ArrayList<>();
+        }
+
+        // 存储所有权限的学生ID,使用Set去重
+        Set<Long> studentIdSet = new HashSet<>();
+
+        // 按照权限分别查询并合并结果(支持用户拥有多种角色的情况)
+        if (managerTypes.contains("1")) {
+            // 全校管理员,获取所有学生
+            List<Long> allStudents = studentDynamicsMapper.getStudentIdsBySchoolAdmin(userId);
+            if (allStudents != null) {
+                studentIdSet.addAll(allStudents);
+            }
+            // 全校管理员可以查看所有学生,直接返回
+            return new ArrayList<>(studentIdSet);
+        }
+
+        // 对于非全校管理员,合并多种权限的结果
+        if (managerTypes.contains("2")) {
+            // 校区管理员,获取管理的校区下的学生
+            List<Long> campusStudents = studentDynamicsMapper.getStudentIdsByCampusAdmin(userId);
+            if (campusStudents != null) {
+                studentIdSet.addAll(campusStudents);
+            }
+        }
+
+        if (managerTypes.contains("3")) {
+            // 宿舍区管理员,获取管理的宿舍区下的学生
+            List<Long> areaStudents = studentDynamicsMapper.getStudentIdsByAreaAdmin(userId);
+            if (areaStudents != null) {
+                studentIdSet.addAll(areaStudents);
+            }
+        }
+
+        if (managerTypes.contains("4")) {
+            // 宿舍楼管理员,获取管理的楼栋下的学生
+            List<Long> buildingStudents = studentDynamicsMapper.getStudentIdsByBuildingAdmin(userId);
+            if (buildingStudents != null) {
+                studentIdSet.addAll(buildingStudents);
+            }
+        }
+
+        return new ArrayList<>(studentIdSet);
+    }
+}

+ 213 - 0
backend/ruoyi-modules/ruoyi-xg/src/main/resources/mapper/xg/XgStudentDynamicsMapper.xml

@@ -0,0 +1,213 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="org.dromara.xg.mapper.XgStudentDynamicsMapper">
+
+    <!-- 检查用户是否有宿舍管理权限 -->
+    <select id="checkUserDormManagerType" resultType="String">
+        SELECT
+            DISTINCT manager_type
+        FROM
+            xg_dorm_manager
+        WHERE
+            user_id = #{userId}
+            AND deleted = '0'
+            AND status = '0'
+    </select>
+
+    <!-- 获取全校管理员权限的所有楼栋ID列表 -->
+    <select id="getBuildingIdsBySchoolAdmin" resultType="Long">
+        SELECT
+            id
+        FROM
+            xg_dorm_building
+        WHERE
+            deleted = '0'
+            AND EXISTS (
+                SELECT 1 FROM xg_dorm_manager m
+                WHERE m.user_id = #{userId}
+                AND m.manager_type = '1'
+                AND m.deleted = '0'
+                AND m.status = '0'
+            )
+    </select>
+
+    <!-- 获取校区管理员权限的楼栋ID列表 -->
+    <select id="getBuildingIdsByCampusAdmin" resultType="Long">
+        SELECT
+            b.id
+        FROM
+            xg_dorm_building b
+        WHERE
+            b.deleted = '0'
+            AND b.campus_id IN (
+                SELECT
+                    s.auth_source
+                FROM
+                    xg_dorm_manager m
+                JOIN
+                    xg_dorm_manager_source s ON m.id = s.manager_id
+                WHERE
+                    m.user_id = #{userId}
+                    AND m.manager_type = '2'
+                    AND m.deleted = '0'
+                    AND m.status = '0'
+                    AND s.deleted = '0'
+                    AND s.status = '0'
+            )
+    </select>
+
+    <!-- 获取宿舍区管理员权限的楼栋ID列表 -->
+    <select id="getBuildingIdsByAreaAdmin" resultType="Long">
+        SELECT
+            b.id
+        FROM
+            xg_dorm_building b
+        WHERE
+            b.deleted = '0'
+            AND b.area_id IN (
+                SELECT
+                    s.auth_source
+                FROM
+                    xg_dorm_manager m
+                JOIN
+                    xg_dorm_manager_source s ON m.id = s.manager_id
+                WHERE
+                    m.user_id = #{userId}
+                    AND m.manager_type = '3'
+                    AND m.deleted = '0'
+                    AND m.status = '0'
+                    AND s.deleted = '0'
+                    AND s.status = '0'
+            )
+    </select>
+
+    <!-- 获取宿舍楼管理员权限的楼栋ID列表 -->
+    <select id="getBuildingIdsByBuildingAdmin" resultType="Long">
+        SELECT
+            b.id
+        FROM
+            xg_dorm_building b
+        WHERE
+            b.deleted = '0'
+            AND b.id IN (
+                SELECT
+                    s.auth_source
+                FROM
+                    xg_dorm_manager m
+                JOIN
+                    xg_dorm_manager_source s ON m.id = s.manager_id
+                WHERE
+                    m.user_id = #{userId}
+                    AND m.manager_type = '4'
+                    AND m.deleted = '0'
+                    AND m.status = '0'
+                    AND s.deleted = '0'
+                    AND s.status = '0'
+            )
+    </select>
+
+    <!-- 获取全校管理员权限的所有学生ID列表 -->
+    <select id="getStudentIdsBySchoolAdmin" resultType="Long">
+        SELECT
+            sm.id
+        FROM
+            xg_student_msg sm
+        WHERE
+            sm.deleted = 0
+            AND EXISTS (
+                SELECT 1 FROM xg_dorm_manager m
+                WHERE m.user_id = #{userId}
+                AND m.manager_type = '1'
+                AND m.deleted = '0'
+                AND m.status = '0'
+            )
+    </select>
+
+    <!-- 获取校区管理员权限的学生ID列表 -->
+    <select id="getStudentIdsByCampusAdmin" resultType="Long">
+        SELECT
+            sm.id
+        FROM
+            xg_student_msg sm
+        INNER JOIN
+            xg_dorm_building db ON sm.dormitory_building = db.id
+        WHERE
+            sm.deleted = 0
+            AND db.deleted = '0'
+            AND db.campus_id IN (
+                SELECT
+                    s.auth_source
+                FROM
+                    xg_dorm_manager m
+                JOIN
+                    xg_dorm_manager_source s ON m.id = s.manager_id
+                WHERE
+                    m.user_id = #{userId}
+                    AND m.manager_type = '2'
+                    AND m.deleted = '0'
+                    AND m.status = '0'
+                    AND s.deleted = '0'
+                    AND s.status = '0'
+            )
+    </select>
+
+    <!-- 获取宿舍区管理员权限的学生ID列表 -->
+    <select id="getStudentIdsByAreaAdmin" resultType="Long">
+        SELECT
+            sm.id
+        FROM
+            xg_student_msg sm
+        INNER JOIN
+            xg_dorm_building db ON sm.dormitory_building = db.id
+        WHERE
+            sm.deleted = 0
+            AND db.deleted = '0'
+            AND db.area_id IN (
+                SELECT
+                    s.auth_source
+                FROM
+                    xg_dorm_manager m
+                JOIN
+                    xg_dorm_manager_source s ON m.id = s.manager_id
+                WHERE
+                    m.user_id = #{userId}
+                    AND m.manager_type = '3'
+                    AND m.deleted = '0'
+                    AND m.status = '0'
+                    AND s.deleted = '0'
+                    AND s.status = '0'
+            )
+    </select>
+
+    <!-- 获取宿舍楼管理员权限的学生ID列表 -->
+    <select id="getStudentIdsByBuildingAdmin" resultType="Long">
+        SELECT
+            sm.id
+        FROM
+            xg_student_msg sm
+        INNER JOIN
+            xg_dorm_building db ON sm.dormitory_building = db.id
+        WHERE
+            sm.deleted = 0
+            AND db.deleted = '0'
+            AND db.id IN (
+                SELECT
+                    s.auth_source
+                FROM
+                    xg_dorm_manager m
+                JOIN
+                    xg_dorm_manager_source s ON m.id = s.manager_id
+                WHERE
+                    m.user_id = #{userId}
+                    AND m.manager_type = '4'
+                    AND m.deleted = '0'
+                    AND m.status = '0'
+                    AND s.deleted = '0'
+                    AND s.status = '0'
+            )
+    </select>
+
+</mapper>
+

+ 269 - 0
web/src/api/xg/studentDynamics.ts

@@ -0,0 +1,269 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+
+/**
+ * 学生动向信息管理(宿管员)API
+ * 这些接口专门用于学生动向Tab页,会自动根据宿管员权限过滤数据
+ */
+
+// ==================== 调宿退宿申请相关接口 ====================
+
+export interface DormApplicationQuery {
+  pageNum?: number;
+  pageSize?: number;
+  studentName?: string;
+  studentNo?: string;
+  collegeName?: string;
+  majorName?: string;
+  className?: string;
+  applicationType?: string;
+  applicationStatus?: string;
+  applyTime?: string;
+  params?: any;
+}
+
+export interface DormApplicationVO {
+  id: string | number;
+  applicationName: string;
+  applicationType: string;
+  applicationStatus: string;
+  studentId: string | number;
+  studentNo: string;
+  studentName: string;
+  studentPhone: string;
+  collegeId: string | number;
+  collegeName: string;
+  majorId: string | number;
+  majorName: string;
+  classId: string | number;
+  className: string;
+  currentCampusId: string | number;
+  currentCampusName: string;
+  currentBuildingId: string | number;
+  currentBuildingName: string;
+  currentRoomId: string | number;
+  currentRoomNo: string;
+  currentBedId: string | number;
+  currentBedNo: string;
+  targetCampusId?: string | number;
+  targetCampusName?: string;
+  targetBuildingId?: string | number;
+  targetBuildingName?: string;
+  targetRoomId?: string | number;
+  targetRoomNo?: string;
+  targetBedId?: string | number;
+  targetBedNo?: string;
+  reasonDetail: string;
+  applyTime: string;
+  attachmentOssIds?: string;
+  remark?: string;
+  createTime: string;
+  updateTime: string;
+}
+
+/**
+ * 查询宿管员权限内的学生调宿退宿申请列表
+ */
+export function listDormApplication(query?: DormApplicationQuery): AxiosPromise<any> {
+  return request({
+    url: '/studentDynamics/dormApplicationList',
+    method: 'get',
+    params: query
+  });
+}
+
+/**
+ * 获取调宿退宿申请详细信息
+ */
+export function getDormApplication(id: string | number): AxiosPromise<DormApplicationVO> {
+  return request({
+    url: '/studentDynamics/dormApplication/' + id,
+    method: 'get'
+  });
+}
+
+/**
+ * 获取调宿退宿申请详细信息(别名)
+ */
+export const getDormApplicationInfo = getDormApplication;
+
+/**
+ * 修改调宿退宿申请信息
+ */
+export function updateDormApplication(data: DormApplicationVO): AxiosPromise<any> {
+  return request({
+    url: '/studentDynamics/dormApplication',
+    method: 'put',
+    data: data
+  });
+}
+
+/**
+ * 删除调宿退宿申请
+ */
+export function delDormApplication(id: string | number | Array<string | number>): AxiosPromise<any> {
+  return request({
+    url: '/studentDynamics/dormApplication/' + id,
+    method: 'delete'
+  });
+}
+
+// ==================== 假期留宿相关接口 ====================
+
+export interface HolidayStayQuery {
+  pageNum?: number;
+  pageSize?: number;
+  studentName?: string;
+  studentNo?: string;
+  collegeName?: string;
+  majorName?: string;
+  className?: string;
+  holidayType?: string;
+  applyStartDate?: string;
+  applyEndDate?: string;
+  status?: string;
+  params?: any;
+}
+
+export interface HolidayStayVO {
+  id: string | number;
+  studentId: string | number;
+  studentName: string;
+  studentNo: string;
+  classId: string | number;
+  className: string;
+  majorId: string | number;
+  majorName: string;
+  collegeId: string | number;
+  collegeName: string;
+  campusId: string | number;
+  campusName: string;
+  areaId?: string | number;
+  areaName?: string;
+  buildingId: string | number;
+  buildingName: string;
+  roomId: string | number;
+  roomName: string;
+  bedId: string | number;
+  bedName: string;
+  holidayType: string;
+  applyStartDate: string;
+  applyEndDate: string;
+  stayReason: string;
+  stayReasonType: string;
+  studentPhone: string;
+  emergencyContact: string;
+  emergencyPhone: string;
+  parentConsent: string;
+  attachmentOssIds?: string;
+  remark?: string;
+  createTime: string;
+  updateTime: string;
+}
+
+/**
+ * 查询宿管员权限内的学生假期留宿信息列表
+ */
+export function listHolidayStay(query?: HolidayStayQuery): AxiosPromise<any> {
+  return request({
+    url: '/studentDynamics/holidayStayList',
+    method: 'get',
+    params: query
+  });
+}
+
+/**
+ * 获取假期留宿详细信息
+ */
+export function getHolidayStay(id: string | number): AxiosPromise<HolidayStayVO> {
+  return request({
+    url: '/studentDynamics/holidayStay/' + id,
+    method: 'get'
+  });
+}
+
+/**
+ * 获取假期留宿详细信息(别名)
+ */
+export const getHolidayStayInfo = getHolidayStay;
+
+// ==================== 学生请假相关接口 ====================
+
+export interface StudentLeaveQuery {
+  pageNum?: number;
+  pageSize?: number;
+  studentName?: string;
+  studentNo?: string;
+  collegeName?: string;
+  majorName?: string;
+  className?: string;
+  leaveTypeName?: string;
+  approvalStatus?: string;
+  startTime?: string;
+  endTime?: string;
+  params?: any;
+}
+
+export interface StudentLeaveVO {
+  id: string | number;
+  studentId: string | number;
+  studentName: string;
+  studentNo: string;
+  classId: string | number;
+  className: string;
+  majorId: string | number;
+  majorName: string;
+  collegeId: string | number;
+  collegeName: string;
+  leaveTypeCode: string;
+  leaveTypeName: string;
+  startTime: string;
+  endTime: string;
+  leaveDuration: number;
+  isLeaveSchool: string;
+  needCancel: string;
+  emergencyContact: string;
+  emergencyPhone: string;
+  leaveReason: string;
+  leaveLocation?: string;
+  attachmentOssIds?: string;
+  approvalStatus: string;
+  approvalOpinion?: string;
+  approvalTime?: string;
+  approvalBy?: string | number;
+  approvalByName?: string;
+  cancelStatus?: string;
+  cancelTime?: string;
+  cancelLocation?: string;
+  cancelPhotoOssIds?: string;
+  cancelRemark?: string;
+  createTime: string;
+  updateTime: string;
+}
+
+/**
+ * 查询宿管员权限内的学生请假信息列表
+ */
+export function listStudentLeave(query?: StudentLeaveQuery): AxiosPromise<any> {
+  return request({
+    url: '/studentDynamics/studentLeaveList',
+    method: 'get',
+    params: query
+  });
+}
+
+/**
+ * 获取学生请假详细信息
+ */
+export function getStudentLeave(id: string | number): AxiosPromise<StudentLeaveVO> {
+  return request({
+    url: '/studentDynamics/studentLeave/' + id,
+    method: 'get'
+  });
+}
+
+/**
+ * 获取学生请假详细信息(别名)
+ */
+export const getStudentLeaveInfo = getStudentLeave;
+

+ 256 - 0
web/src/views/xg/studentDynamics/components/DormApplicationDetail.vue

@@ -0,0 +1,256 @@
+<template>
+  <el-dialog :title="dialogTitle" v-model="dialogOpen" width="800px" append-to-body @close="cancel">
+    <el-form ref="formRef" :model="form" :rules="rules" label-width="120px" v-loading="loading">
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="申请名称:">
+            <span>{{ form.applicationName }}</span>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="申请类型:">
+            <dict-tag :options="dorm_application_type" :value="form.applicationType" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="学生姓名:">
+            <span>{{ form.studentName }}</span>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="学号:">
+            <span>{{ form.studentNo }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="学院:">
+            <span>{{ form.collegeName }}</span>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="专业:">
+            <span>{{ form.majorName }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="班级:">
+            <span>{{ form.className }}</span>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="联系电话:">
+            <span>{{ form.studentPhone }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="24">
+          <el-form-item label="当前宿舍:">
+            <span>{{ form.currentCampusName }} {{ form.currentBuildingName }} {{ form.currentRoomNo }} {{ form.currentBedNo }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20" v-if="form.applicationType === '1'">
+        <el-col :span="24">
+          <el-form-item label="目标宿舍:">
+            <span>{{ form.targetCampusName }} {{ form.targetBuildingName }} {{ form.targetRoomNo }} {{ form.targetBedNo }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="申请时间:">
+            <span>{{ form.applyTime }}</span>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="审批状态:">
+            <dict-tag :options="student_leave_approval_status" :value="form.applicationStatus" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="24">
+          <el-form-item label="申请原因:" prop="reasonDetail">
+            <el-input
+              v-if="isEditable"
+              v-model="form.reasonDetail"
+              type="textarea"
+              :rows="3"
+              placeholder="请输入申请原因"
+              maxlength="500"
+              show-word-limit
+            />
+            <span v-else>{{ form.reasonDetail || '无' }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="24">
+          <el-form-item label="备注信息:" prop="remark">
+            <el-input
+              v-if="isEditable"
+              v-model="form.remark"
+              type="textarea"
+              :rows="2"
+              placeholder="请输入备注信息"
+              maxlength="200"
+              show-word-limit
+            />
+            <span v-else>{{ form.remark || '无' }}</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="cancel">{{ isEditable ? '取消' : '关闭' }}</el-button>
+        <el-button v-if="isEditable" type="primary" @click="submitForm" :loading="submitLoading">保存</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts" name="DormApplicationDetail">
+import { getCurrentInstance, ref, toRefs, computed, type ComponentInternalInstance } from 'vue';
+import { ElMessage } from 'element-plus';
+import { getDormApplicationInfo, updateDormApplication, type DormApplicationVO } from '@/api/xg/studentDynamics';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { dorm_application_type, student_leave_approval_status } = toRefs(
+  proxy?.useDict('dorm_application_type', 'student_leave_approval_status') || {}
+);
+
+const emit = defineEmits(['refresh']);
+
+// 弹窗状态
+const dialogOpen = ref(false);
+const loading = ref(false);
+const submitLoading = ref(false);
+
+// 表单数据
+const form = ref<DormApplicationVO>({} as DormApplicationVO);
+
+// 表单引用
+const formRef = ref();
+
+// 表单校验规则
+const rules = {
+  reasonDetail: [{ required: true, message: '请输入申请原因', trigger: 'blur' }]
+};
+
+/**
+ * 是否可编辑
+ * 草稿、已撤销、已退回状态可编辑
+ */
+const isEditable = computed(() => {
+  return ['draft', 'cancel', 'back'].includes(form.value.applicationStatus || '');
+});
+
+/**
+ * 弹窗标题
+ */
+const dialogTitle = computed(() => {
+  return isEditable.value ? '修改调宿/退宿申请' : '调宿/退宿申请详情';
+});
+
+/**
+ * 打开弹窗
+ */
+const openDialog = (id: number) => {
+  reset();
+  getDetail(id);
+  dialogOpen.value = true;
+};
+
+/**
+ * 获取详情数据
+ */
+const getDetail = async (id: number) => {
+  loading.value = true;
+  try {
+    const response = await getDormApplicationInfo(id);
+    form.value = response.data;
+  } catch (error) {
+    console.error('获取调宿退宿申请详情失败:', error);
+    ElMessage.error('获取详情失败');
+  } finally {
+    loading.value = false;
+  }
+};
+
+/**
+ * 重置表单
+ */
+const reset = () => {
+  form.value = {} as DormApplicationVO;
+  formRef.value?.resetFields();
+};
+
+/**
+ * 提交表单
+ */
+const submitForm = async () => {
+  if (!formRef.value) return;
+
+  await formRef.value.validate(async (valid: boolean) => {
+    if (!valid) return;
+
+    submitLoading.value = true;
+    try {
+      await updateDormApplication(form.value);
+      ElMessage.success('保存成功');
+      dialogOpen.value = false;
+      emit('refresh');
+    } catch (error) {
+      console.error('保存失败:', error);
+      ElMessage.error('保存失败');
+    } finally {
+      submitLoading.value = false;
+    }
+  });
+};
+
+/**
+ * 取消操作
+ */
+const cancel = () => {
+  dialogOpen.value = false;
+  reset();
+};
+
+// 暴露方法
+defineExpose({
+  open: openDialog
+});
+</script>
+
+<style scoped>
+.dialog-footer {
+  text-align: right;
+}
+
+:deep(.el-form-item__label) {
+  font-weight: 600;
+  color: #606266;
+}
+
+:deep(.el-form-item__content) {
+  color: #303133;
+}
+</style>

+ 258 - 0
web/src/views/xg/studentDynamics/components/DormApplicationTab.vue

@@ -0,0 +1,258 @@
+<template>
+  <div class="page-container">
+    <!-- 搜索表单 -->
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" style="margin-bottom: 10px">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" label-width="100px">
+            <el-row :gutter="20">
+              <el-col :span="6">
+                <el-form-item label="学生姓名" prop="studentName">
+                  <el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="学号" prop="studentNo">
+                  <el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="学院名称" prop="collegeName">
+                  <el-input v-model="queryParams.collegeName" placeholder="请输入学院名称" clearable @keyup.enter="handleQuery" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="专业名称" prop="majorName">
+                  <el-input v-model="queryParams.majorName" placeholder="请输入专业名称" clearable @keyup.enter="handleQuery" />
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20">
+              <el-col :span="6">
+                <el-form-item label="班级名称" prop="className">
+                  <el-input v-model="queryParams.className" placeholder="请输入班级名称" clearable @keyup.enter="handleQuery" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="申请类型" prop="applicationType">
+                  <el-select v-model="queryParams.applicationType" placeholder="请选择申请类型" clearable style="width: 100%">
+                    <el-option v-for="dict in dorm_application_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="申请时间" prop="applyTime">
+                  <el-date-picker
+                    clearable
+                    v-model="queryParams.applyTime"
+                    type="date"
+                    style="width: 100%"
+                    value-format="YYYY-MM-DD"
+                    placeholder="请选择申请时间"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="审批状态" prop="applicationStatus">
+                  <el-select v-model="queryParams.applicationStatus" placeholder="请选择审批状态" clearable style="width: 100%">
+                    <el-option v-for="dict in student_leave_approval_status" :key="dict.value" :label="dict.label" :value="dict.value" />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row>
+              <el-col :span="24" class="search-btn-container">
+                <el-form-item>
+                  <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+                  <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <!-- 数据表格 -->
+    <el-card shadow="never">
+      <template #header>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <span class="card-title">调宿/退宿申请列表</span>
+          </el-col>
+          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
+        <el-table-column label="学生姓名" align="center" prop="studentName" />
+        <el-table-column label="学号" align="center" prop="studentNo" />
+        <el-table-column label="学院" align="center" prop="collegeName" show-overflow-tooltip />
+        <el-table-column label="专业" align="center" prop="majorName" show-overflow-tooltip />
+        <el-table-column label="班级" align="center" prop="className" show-overflow-tooltip />
+        <el-table-column label="申请类型" align="center">
+          <template #default="scope">
+            <dict-tag :options="dorm_application_type" :value="scope.row.applicationType" />
+          </template>
+        </el-table-column>
+        <el-table-column label="当前宿舍" align="center" show-overflow-tooltip>
+          <template #default="scope"> {{ scope.row.currentBuildingName }}{{ scope.row.currentRoomNo }}{{ scope.row.currentBedNo }} </template>
+        </el-table-column>
+        <el-table-column label="目标宿舍" align="center" show-overflow-tooltip>
+          <template #default="scope">
+            <span v-if="scope.row.applicationType === '1'">
+              {{ scope.row.targetBuildingName }}{{ scope.row.targetRoomNo }}{{ scope.row.targetBedNo }}
+            </span>
+            <span v-else>—</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="审批状态" align="center">
+          <template #default="scope">
+            <dict-tag :options="student_leave_approval_status" :value="scope.row.applicationStatus" />
+          </template>
+        </el-table-column>
+        <el-table-column label="申请时间" align="center" prop="applyTime" width="180" />
+        <el-table-column label="操作" align="center" width="150" fixed="right">
+          <template #default="scope">
+            <el-tooltip content="查看" placement="top">
+              <el-button
+                link
+                type="primary"
+                icon="View"
+                @click="handleView(scope.row)"
+                v-hasPermi="['studentDynamics:dormApplication:query']"
+              ></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts" name="DormApplicationTab">
+import { getCurrentInstance, ref, onMounted, toRefs, type ComponentInternalInstance } from 'vue';
+import { listDormApplication, type DormApplicationQuery } from '@/api/xg/studentDynamics';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { dorm_application_type, student_leave_approval_status } = toRefs(
+  proxy?.useDict('dorm_application_type', 'student_leave_approval_status') || {}
+);
+
+// 数据状态
+const loading = ref(true);
+const showSearch = ref(true);
+const dataList = ref([]);
+const total = ref(0);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+
+// 查询参数
+const queryParams = ref<DormApplicationQuery & any>({
+  pageNum: 1,
+  pageSize: 10,
+  studentName: undefined,
+  studentNo: undefined,
+  collegeName: undefined,
+  majorName: undefined,
+  className: undefined,
+  applicationType: undefined,
+  applicationStatus: undefined,
+  applyTime: undefined
+});
+
+// 表单引用
+const queryFormRef = ref();
+
+/**
+ * 查询列表数据
+ */
+const getList = async () => {
+  loading.value = true;
+  try {
+    const response = await listDormApplication(queryParams.value);
+    dataList.value = response.rows;
+    total.value = response.total;
+  } catch (error) {
+    console.error('获取调宿退宿申请列表失败:', error);
+    proxy?.$modal.msgError('获取数据失败');
+  } finally {
+    loading.value = false;
+  }
+};
+
+/**
+ * 搜索按钮操作
+ */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+/**
+ * 重置按钮操作
+ */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+/**
+ * 多选框选中数据
+ */
+const handleSelectionChange = (selection: any) => {
+  ids.value = selection.map((item: any) => item.id);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+};
+
+/**
+ * 查看详情
+ */
+const handleView = (row: any) => {
+  // 跳转到调宿退宿申请详情页面
+  proxy.$router.push({
+    path: `/xg/xsjcxx/xssssq/dialog`,
+    query: {
+      id: row.id,
+      type: 'update'
+    }
+  });
+};
+
+// 页面初始化
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped>
+/* 页面容器样式 */
+.page-container {
+  padding: 0;
+}
+
+.mb8 {
+  margin-bottom: 8px;
+}
+
+/* 搜索按钮容器样式 */
+.search-btn-container {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 10px;
+}
+
+.search-btn-container .el-form-item {
+  margin-bottom: 0 !important;
+}
+
+.card-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+}
+</style>

+ 474 - 0
web/src/views/xg/studentDynamics/components/HolidayStayDetail.vue

@@ -0,0 +1,474 @@
+<template>
+  <el-dialog title="假期留宿详情" v-model="dialogOpen" width="1000px" append-to-body>
+    <el-form :model="form" label-width="120px" v-loading="loading">
+      <el-row :gutter="20">
+        <!-- 左列:学生基本信息 -->
+        <el-col :span="12">
+          <el-form-item label="学生姓名">
+            <el-input v-model="form.studentName" disabled />
+          </el-form-item>
+          <el-form-item label="学号">
+            <el-input v-model="form.studentNo" disabled />
+          </el-form-item>
+          <el-form-item label="学生联系电话">
+            <el-input v-model="form.studentPhone" disabled />
+          </el-form-item>
+          <el-form-item label="学院名称">
+            <el-input v-model="form.collegeName" disabled />
+          </el-form-item>
+          <el-form-item label="专业名称">
+            <el-input v-model="form.majorName" disabled />
+          </el-form-item>
+          <el-form-item label="班级名称">
+            <el-input v-model="form.className" disabled />
+          </el-form-item>
+        </el-col>
+
+        <!-- 右列:宿舍信息 -->
+        <el-col :span="12">
+          <el-form-item label="校区名称">
+            <el-input v-model="form.campusName" disabled />
+          </el-form-item>
+          <el-form-item label="宿舍区名称">
+            <el-input v-model="form.areaName" disabled />
+          </el-form-item>
+          <el-form-item label="宿舍楼名称">
+            <el-input v-model="form.buildingName" disabled />
+          </el-form-item>
+          <el-form-item label="房间名称">
+            <el-input v-model="form.roomName" disabled />
+          </el-form-item>
+          <el-form-item label="床位名称">
+            <el-input v-model="form.bedName" disabled />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <!-- 假期申请信息 -->
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="假期类型">
+            <el-input v-model="holidayTypeLabel" disabled />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="留宿原因类型">
+            <el-input v-model="stayReasonTypeLabel" disabled />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="留宿开始日期">
+            <el-input v-model="form.applyStartDate" disabled />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="留宿结束日期">
+            <el-input v-model="form.applyEndDate" disabled />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <!-- 联系信息 -->
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="紧急联系人姓名">
+            <el-input v-model="form.emergencyContact" disabled />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="紧急联系人电话">
+            <el-input v-model="form.emergencyPhone" disabled />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="家长同意">
+            <el-input v-model="parentConsentLabel" disabled />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <!-- 留宿原因 -->
+      <el-form-item label="留宿原因">
+        <el-input v-model="form.stayReason" type="textarea" :rows="3" disabled />
+      </el-form-item>
+
+      <!-- 附件显示 -->
+      <el-form-item label="附件">
+        <div class="attachment-view-container" v-if="attachmentList.length > 0">
+          <div class="attachment-grid">
+            <div v-for="file in attachmentList" :key="file.ossId" class="attachment-item">
+              <!-- 缩略图区域 -->
+              <div class="attachment-thumb">
+                <image-preview v-if="isImageFile(file)" :src="file.ossId" class="thumb-image" :isOssId="true" />
+                <div v-else class="file-placeholder">
+                  <el-icon><Document /></el-icon>
+                  <span class="file-ext">{{ getFileExt(file.name) }}</span>
+                </div>
+              </div>
+
+              <!-- 文件名称 -->
+              <div class="attachment-name" :title="file.name">
+                {{ getShortFileName(file.name) }}
+              </div>
+
+              <!-- 操作按钮 -->
+              <div class="attachment-actions">
+                <el-button type="success" size="small" @click="handleDownload(file)" title="下载">
+                  <el-icon><Download /></el-icon>
+                </el-button>
+              </div>
+            </div>
+          </div>
+        </div>
+        <div v-else class="no-attachment">
+          <el-empty description="暂无附件" :image-size="60" />
+        </div>
+      </el-form-item>
+
+      <!-- 备注信息 -->
+      <el-form-item label="备注信息">
+        <el-input v-model="form.remark" type="textarea" :rows="3" disabled />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="dialogOpen = false">关闭</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts" name="HolidayStayDetail">
+import { ref, computed, toRefs, getCurrentInstance, type ComponentInternalInstance } from 'vue';
+import ElMessage from 'element-plus/es/components/message/index';
+import ElLoading from 'element-plus/es/components/loading/index';
+import { Document, Download } from '@element-plus/icons-vue';
+import { getHolidayStay, type HolidayStayVO } from '@/api/xg/studentDynamics';
+import ImagePreview from '@/components/ImagePreview/index.vue';
+import request from '@/utils/request';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+// 字典数据
+const { xg_holiday_type, xg_stay_reason_type, sys_yesno } = toRefs<any>(proxy?.useDict('xg_holiday_type', 'xg_stay_reason_type', 'sys_yesno'));
+
+// 对话框状态
+const dialogOpen = ref(false);
+const loading = ref(false);
+
+// 表单数据
+const form = ref<Partial<HolidayStayVO>>({
+  id: undefined,
+  studentId: undefined,
+  studentName: '',
+  studentNo: '',
+  classId: undefined,
+  className: '',
+  majorId: undefined,
+  majorName: '',
+  collegeId: undefined,
+  collegeName: '',
+  campusId: undefined,
+  campusName: '',
+  areaId: undefined,
+  areaName: '',
+  buildingId: undefined,
+  buildingName: '',
+  roomId: undefined,
+  roomName: '',
+  bedId: undefined,
+  bedName: '',
+  holidayType: '',
+  applyStartDate: '',
+  applyEndDate: '',
+  stayReason: '',
+  stayReasonType: '',
+  studentPhone: '',
+  emergencyContact: '',
+  emergencyPhone: '',
+  parentConsent: '',
+  attachmentOssIds: '',
+  remark: ''
+});
+
+// 附件列表
+const attachmentList = ref<any[]>([]);
+
+// 字典标签计算属性
+const holidayTypeLabel = computed(() => {
+  if (!form.value.holidayType || !xg_holiday_type.value) return '';
+  const dict = xg_holiday_type.value.find((item: any) => item.value === form.value.holidayType);
+  return dict ? dict.label : form.value.holidayType;
+});
+
+const stayReasonTypeLabel = computed(() => {
+  if (!form.value.stayReasonType || !xg_stay_reason_type.value) return '';
+  const dict = xg_stay_reason_type.value.find((item: any) => item.value === form.value.stayReasonType);
+  return dict ? dict.label : form.value.stayReasonType;
+});
+
+const parentConsentLabel = computed(() => {
+  if (!form.value.parentConsent || !sys_yesno.value) return '';
+  const dict = sys_yesno.value.find((item: any) => item.value === form.value.parentConsent);
+  return dict ? dict.label : form.value.parentConsent;
+});
+
+/**
+ * 打开详情弹窗
+ */
+const open = async (id: string | number) => {
+  loading.value = true;
+  dialogOpen.value = true;
+
+  try {
+    // 获取详情数据
+    const response = await getHolidayStay(id);
+    form.value = response.data;
+
+    // 处理附件数据
+    await handleAttachmentData();
+  } catch (error) {
+    console.error('获取假期留宿详情失败:', error);
+    ElMessage.error('获取详情失败');
+    dialogOpen.value = false;
+  } finally {
+    loading.value = false;
+  }
+};
+
+/**
+ * 处理附件数据
+ */
+const handleAttachmentData = async () => {
+  try {
+    if (form.value.attachmentOssIds && form.value.attachmentOssIds.toString().trim()) {
+      // 将逗号分隔的字符串转换为数组
+      const ossIdsArray = form.value.attachmentOssIds
+        .toString()
+        .split(',')
+        .map((id) => id.trim())
+        .filter((id) => id);
+
+      // 调用接口获取附件信息
+      const response = await request({
+        url: `/resource/oss/listByIds/${ossIdsArray.join(',')}`,
+        method: 'get'
+      });
+
+      if (response.code === 200 && response.data) {
+        // 将附件信息转换为显示需要的格式
+        attachmentList.value = response.data.map((item: any) => ({
+          ossId: item.ossId,
+          name: item.originalName || '附件',
+          url: item.url,
+          type: item.contentType || ''
+        }));
+      } else {
+        attachmentList.value = [];
+      }
+    } else {
+      attachmentList.value = [];
+    }
+  } catch (error) {
+    console.error('处理附件数据失败:', error);
+    attachmentList.value = [];
+  }
+};
+
+/**
+ * 判断是否为图片文件
+ */
+const isImageFile = (file: any) => {
+  const imageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp'];
+  return imageTypes.includes(file.type) || /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(file.name);
+};
+
+/**
+ * 获取文件扩展名
+ */
+const getFileExt = (fileName: string) => {
+  const ext = fileName.split('.').pop()?.toUpperCase();
+  return ext || 'FILE';
+};
+
+/**
+ * 获取缩短的文件名
+ */
+const getShortFileName = (fileName: string) => {
+  if (!fileName) return '';
+
+  const lastDotIndex = fileName.lastIndexOf('.');
+  if (lastDotIndex === -1) {
+    return fileName.length > 12 ? fileName.substring(0, 12) + '...' : fileName;
+  }
+
+  const name = fileName.substring(0, lastDotIndex);
+  const ext = fileName.substring(lastDotIndex);
+
+  if (name.length <= 8) {
+    return fileName;
+  }
+
+  return name.substring(0, 8) + '...' + ext;
+};
+
+/**
+ * 下载附件
+ */
+const handleDownload = async (file: any) => {
+  try {
+    const loading = ElLoading.service({
+      text: '正在下载文件...',
+      target: document.body
+    });
+
+    const response = await request({
+      url: '/resource/oss/download/' + file.ossId,
+      method: 'get',
+      responseType: 'blob',
+      timeout: 300000
+    });
+
+    // 验证响应数据
+    if (!response) {
+      throw new Error('响应数据为空');
+    }
+
+    // 检查response的结构
+    let blobData: Blob;
+    if (response instanceof Blob) {
+      blobData = response;
+    } else if (response.data instanceof Blob) {
+      blobData = response.data;
+    } else {
+      throw new Error('无效的响应数据格式');
+    }
+
+    const url = window.URL.createObjectURL(blobData);
+    const link = document.createElement('a');
+    link.href = url;
+    link.download = file.name;
+    link.style.display = 'none';
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+    window.URL.revokeObjectURL(url);
+    loading?.close();
+    ElMessage.success('下载成功');
+  } catch (error) {
+    console.error('下载文件失败:', error);
+    ElMessage.error('下载文件失败,请稍后重试');
+  }
+};
+
+defineExpose({ open });
+</script>
+
+<style scoped>
+/* 附件查看容器样式 */
+.attachment-view-container {
+  width: 100%;
+  padding: 12px;
+  border: 1px solid #e4e7ed;
+  border-radius: 6px;
+  background: #fafafa;
+}
+
+.attachment-grid {
+  display: grid;
+  grid-template-columns: repeat(5, 1fr);
+  gap: 12px;
+}
+
+.attachment-item {
+  background: #fff;
+  border-radius: 6px;
+  overflow: hidden;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  transition: all 0.3s;
+}
+
+.attachment-item:hover {
+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
+}
+
+.attachment-thumb {
+  position: relative;
+  width: 100%;
+  height: 110px;
+  background: #f5f5f5;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow: hidden;
+}
+
+.thumb-image {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.file-placeholder {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  color: #909399;
+  font-size: 14px;
+}
+
+.file-placeholder .el-icon {
+  font-size: 28px;
+  margin-bottom: 4px;
+}
+
+.file-ext {
+  font-size: 12px;
+  text-transform: uppercase;
+}
+
+.attachment-name {
+  padding: 6px 8px;
+  font-size: 11px;
+  color: #303133;
+  text-align: center;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  background: #fafafa;
+  border-top: 1px solid #f0f0f0;
+  line-height: 1.2;
+  height: 32px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.attachment-actions {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 6px 8px;
+  background: #fafafa;
+  border-top: 1px solid #f0f0f0;
+  height: 38px;
+}
+
+.attachment-actions .el-button {
+  padding: 4px 12px;
+  font-size: 12px;
+  border-radius: 4px;
+}
+
+.no-attachment {
+  text-align: center;
+  padding: 20px;
+  color: #909399;
+}
+</style>

+ 263 - 0
web/src/views/xg/studentDynamics/components/HolidayStayTab.vue

@@ -0,0 +1,263 @@
+<template>
+  <div class="page-container">
+    <!-- 搜索表单 -->
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" label-width="100px">
+            <el-row :gutter="20">
+              <el-col :span="6">
+                <el-form-item label="学生姓名" prop="studentName">
+                  <el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="学号" prop="studentNo">
+                  <el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="学院名称" prop="collegeName">
+                  <el-input v-model="queryParams.collegeName" placeholder="请输入学院名称" clearable @keyup.enter="handleQuery" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="专业名称" prop="majorName">
+                  <el-input v-model="queryParams.majorName" placeholder="请输入专业名称" clearable @keyup.enter="handleQuery" />
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20">
+              <el-col :span="6">
+                <el-form-item label="班级名称" prop="className">
+                  <el-input v-model="queryParams.className" placeholder="请输入班级名称" clearable @keyup.enter="handleQuery" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="假期类型" prop="holidayType">
+                  <el-select v-model="queryParams.holidayType" placeholder="请选择假期类型" clearable style="width: 100%">
+                    <el-option v-for="dict in xg_holiday_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="开始日期" prop="applyStartDate">
+                  <el-date-picker
+                    clearable
+                    v-model="queryParams.applyStartDate"
+                    type="date"
+                    style="width: 100%"
+                    value-format="YYYY-MM-DD"
+                    placeholder="请选择开始日期"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="结束日期" prop="applyEndDate">
+                  <el-date-picker
+                    clearable
+                    v-model="queryParams.applyEndDate"
+                    type="date"
+                    style="width: 100%"
+                    value-format="YYYY-MM-DD"
+                    placeholder="请选择结束日期"
+                  />
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row>
+              <el-col :span="24" class="search-btn-container">
+                <el-form-item>
+                  <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+                  <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <!-- 数据表格 -->
+    <el-card shadow="never">
+      <template #header>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <span class="card-title">假期留宿信息列表</span>
+          </el-col>
+          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
+        <el-table-column label="学生姓名" align="center" prop="studentName" />
+        <el-table-column label="学号" align="center" prop="studentNo" />
+        <el-table-column label="学院" align="center" prop="collegeName" show-overflow-tooltip />
+        <el-table-column label="专业" align="center" prop="majorName" show-overflow-tooltip />
+        <el-table-column label="班级" align="center" prop="className" show-overflow-tooltip />
+        <el-table-column label="宿舍信息" align="center" show-overflow-tooltip>
+          <template #default="scope"> {{ scope.row.buildingName }}{{ scope.row.roomName }} </template>
+        </el-table-column>
+        <el-table-column label="假期类型" align="center" prop="holidayType">
+          <template #default="scope">
+            <dict-tag :options="xg_holiday_type" :value="scope.row.holidayType" />
+          </template>
+        </el-table-column>
+        <el-table-column label="留宿时间" align="center" show-overflow-tooltip>
+          <template #default="scope"> {{ scope.row.applyStartDate }} 至 {{ scope.row.applyEndDate }} </template>
+        </el-table-column>
+        <el-table-column label="留宿原因类型" align="center" prop="stayReasonType">
+          <template #default="scope">
+            <dict-tag :options="xg_stay_reason_type" :value="scope.row.stayReasonType" />
+          </template>
+        </el-table-column>
+        <el-table-column label="联系电话" align="center" prop="studentPhone" />
+        <el-table-column label="紧急联系人" align="center" prop="emergencyContact" />
+        <el-table-column label="操作" align="center" width="150" fixed="right">
+          <template #default="scope">
+            <el-tooltip content="查看" placement="top">
+              <el-button
+                link
+                type="primary"
+                icon="View"
+                @click="handleView(scope.row)"
+                v-hasPermi="['studentDynamics:holidayStay:query']"
+              ></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+
+    <!-- 查看详情弹窗 -->
+    <HolidayStayDetail ref="detailRef" @refresh="getList" />
+  </div>
+</template>
+
+<script setup lang="ts" name="HolidayStayTab">
+import { getCurrentInstance, ref, onMounted, toRefs } from 'vue';
+import { ElMessage } from 'element-plus';
+import { listHolidayStay, type HolidayStayQuery } from '@/api/xg/studentDynamics';
+import HolidayStayDetail from './HolidayStayDetail.vue';
+
+const { proxy } = getCurrentInstance();
+
+// 字典数据
+const { xg_holiday_type, xg_stay_reason_type } = toRefs(proxy?.useDict('xg_holiday_type', 'xg_stay_reason_type') || {});
+
+// 数据状态
+const loading = ref(true);
+const showSearch = ref(true);
+const dataList = ref([]);
+const total = ref(0);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+
+// 查询参数
+const queryParams = ref<HolidayStayQuery & any>({
+  pageNum: 1,
+  pageSize: 10,
+  studentName: undefined,
+  studentNo: undefined,
+  collegeName: undefined,
+  majorName: undefined,
+  className: undefined,
+  holidayType: undefined,
+  applyStartDate: undefined,
+  applyEndDate: undefined,
+  status: undefined
+});
+
+// 表单引用
+const queryFormRef = ref();
+const detailRef = ref();
+
+/**
+ * 查询列表数据
+ */
+const getList = async () => {
+  loading.value = true;
+  try {
+    const response = await listHolidayStay(queryParams.value);
+    dataList.value = response.rows;
+    total.value = response.total;
+  } catch (error) {
+    console.error('获取假期留宿信息列表失败:', error);
+    ElMessage.error('获取数据失败');
+  } finally {
+    loading.value = false;
+  }
+};
+
+/**
+ * 搜索按钮操作
+ */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+/**
+ * 重置按钮操作
+ */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+/**
+ * 多选框选中数据
+ */
+const handleSelectionChange = (selection: any) => {
+  ids.value = selection.map((item: any) => item.id);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+};
+
+/**
+ * 查看详情
+ */
+const handleView = (row: any) => {
+  detailRef.value?.open(row.id);
+};
+
+// 页面初始化
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped>
+/* 页面容器样式 */
+.page-container {
+  padding: 0;
+}
+
+.mb-[10px] {
+  margin-bottom: 10px;
+}
+
+.mb8 {
+  margin-bottom: 8px;
+}
+
+/* 搜索按钮容器样式 */
+.search-btn-container {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 10px;
+}
+
+.search-btn-container .el-form-item {
+  margin-bottom: 0 !important;
+}
+
+.card-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+}
+</style>

+ 21 - 0
web/src/views/xg/studentDynamics/components/StudentLeaveDetail.vue

@@ -0,0 +1,21 @@
+<template>
+  <el-dialog title="学生请假详情" v-model="dialogOpen" width="800px" append-to-body>
+    <div>学生请假详情组件待实现</div>
+    <template #footer>
+      <el-button @click="dialogOpen = false">关闭</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts" name="StudentLeaveDetail">
+import { ref } from 'vue';
+
+const dialogOpen = ref(false);
+
+const open = (id: number) => {
+  console.log('打开学生请假详情,ID:', id);
+  dialogOpen.value = true;
+};
+
+defineExpose({ open });
+</script>

+ 271 - 0
web/src/views/xg/studentDynamics/components/StudentLeaveTab.vue

@@ -0,0 +1,271 @@
+<template>
+  <div class="page-container">
+    <!-- 搜索表单 -->
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" style="margin-bottom: 10px">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" label-width="100px">
+            <el-row :gutter="20">
+              <el-col :span="6">
+                <el-form-item label="学生姓名" prop="studentName">
+                  <el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="学号" prop="studentNo">
+                  <el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="学院名称" prop="collegeName">
+                  <el-input v-model="queryParams.collegeName" placeholder="请输入学院名称" clearable @keyup.enter="handleQuery" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="专业名称" prop="majorName">
+                  <el-input v-model="queryParams.majorName" placeholder="请输入专业名称" clearable @keyup.enter="handleQuery" />
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20">
+              <el-col :span="6">
+                <el-form-item label="班级名称" prop="className">
+                  <el-input v-model="queryParams.className" placeholder="请输入班级名称" clearable @keyup.enter="handleQuery" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="请假类型" prop="leaveTypeName">
+                  <el-input v-model="queryParams.leaveTypeName" placeholder="请输入请假类型名称" clearable @keyup.enter="handleQuery" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="开始时间" prop="startTime">
+                  <el-date-picker
+                    clearable
+                    v-model="queryParams.startTime"
+                    type="date"
+                    style="width: 100%"
+                    value-format="YYYY-MM-DD"
+                    placeholder="请选择开始时间"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6">
+                <el-form-item label="结束时间" prop="endTime">
+                  <el-date-picker
+                    clearable
+                    v-model="queryParams.endTime"
+                    type="date"
+                    style="width: 100%"
+                    value-format="YYYY-MM-DD"
+                    placeholder="请选择结束时间"
+                  />
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20">
+              <el-col :span="6">
+                <el-form-item label="审批状态" prop="approvalStatus">
+                  <el-select v-model="queryParams.approvalStatus" placeholder="请选择审批状态" clearable style="width: 100%">
+                    <el-option v-for="dict in student_leave_approval_status" :key="dict.value" :label="dict.label" :value="dict.value" />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row>
+              <el-col :span="24" class="search-btn-container">
+                <el-form-item>
+                  <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+                  <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <!-- 数据表格 -->
+    <el-card shadow="never">
+      <template #header>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <span class="card-title">学生请假信息列表</span>
+          </el-col>
+          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
+        <el-table-column label="学生姓名" align="center" prop="studentName" />
+        <el-table-column label="学号" align="center" prop="studentNo" />
+        <el-table-column label="学院" align="center" prop="collegeName" show-overflow-tooltip />
+        <el-table-column label="专业" align="center" prop="majorName" show-overflow-tooltip />
+        <el-table-column label="班级" align="center" prop="className" show-overflow-tooltip />
+        <el-table-column label="请假类型" align="center" prop="leaveTypeName" />
+        <el-table-column label="请假时间" align="center" show-overflow-tooltip>
+          <template #default="scope"> {{ scope.row.startTime }} 至 {{ scope.row.endTime }} </template>
+        </el-table-column>
+        <el-table-column label="请假时长" align="center">
+          <template #default="scope"> {{ scope.row.leaveDuration }}天 </template>
+        </el-table-column>
+        <el-table-column label="是否离校" align="center">
+          <template #default="scope">
+            <dict-tag :options="sys_yesno" :value="scope.row.isLeaveSchool" />
+          </template>
+        </el-table-column>
+        <el-table-column label="审批状态" align="center">
+          <template #default="scope">
+            <dict-tag :options="student_leave_approval_status" :value="scope.row.approvalStatus" />
+          </template>
+        </el-table-column>
+        <el-table-column label="销假状态" align="center">
+          <template #default="scope">
+            <dict-tag :options="xg_xsqj_cancel_status" :value="scope.row.cancelStatus" />
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="150" fixed="right">
+          <template #default="scope">
+            <el-tooltip content="查看" placement="top">
+              <el-button
+                link
+                type="primary"
+                icon="View"
+                @click="handleView(scope.row)"
+                v-hasPermi="['studentDynamics:studentLeave:query']"
+              ></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts" name="StudentLeaveTab">
+import { getCurrentInstance, ref, onMounted, toRefs } from 'vue';
+import { listStudentLeave, type StudentLeaveQuery } from '@/api/xg/studentDynamics';
+
+const { proxy } = getCurrentInstance();
+const { sys_yesno, student_leave_approval_status, xg_xsqj_cancel_status } = toRefs(
+  proxy?.useDict('sys_yesno', 'student_leave_approval_status', 'xg_xsqj_cancel_status') || {}
+);
+
+// 数据状态
+const loading = ref(true);
+const showSearch = ref(true);
+const dataList = ref([]);
+const total = ref(0);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+
+// 查询参数
+const queryParams = ref<StudentLeaveQuery & any>({
+  pageNum: 1,
+  pageSize: 10,
+  studentName: undefined,
+  studentNo: undefined,
+  collegeName: undefined,
+  majorName: undefined,
+  className: undefined,
+  leaveTypeName: undefined,
+  approvalStatus: undefined,
+  startTime: undefined,
+  endTime: undefined
+});
+
+// 表单引用
+const queryFormRef = ref();
+
+/**
+ * 查询列表数据
+ */
+const getList = async () => {
+  loading.value = true;
+  try {
+    const response = await listStudentLeave(queryParams.value);
+    dataList.value = response.rows;
+    total.value = response.total;
+  } catch (error) {
+    console.error('获取学生请假信息列表失败:', error);
+    proxy?.$modal.msgError('获取数据失败');
+  } finally {
+    loading.value = false;
+  }
+};
+
+/**
+ * 搜索按钮操作
+ */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+/**
+ * 重置按钮操作
+ */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+/**
+ * 多选框选中数据
+ */
+const handleSelectionChange = (selection: any) => {
+  ids.value = selection.map((item: any) => item.id);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+};
+
+/**
+ * 查看详情
+ */
+const handleView = (row: any) => {
+  // 跳转到学生请假详情页面
+  proxy.$router.push({
+    path: `/xg/xsjcxx/xsqj/leaveEdit/index`,
+    query: {
+      id: row.id,
+      type: 'update'
+    }
+  });
+};
+
+// 页面初始化
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped>
+/* 页面容器样式 */
+.page-container {
+  padding: 0;
+}
+
+.mb8 {
+  margin-bottom: 8px;
+}
+
+/* 搜索按钮容器样式 */
+.search-btn-container {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 10px;
+}
+
+.search-btn-container .el-form-item {
+  margin-bottom: 0 !important;
+}
+
+.card-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+}
+</style>

+ 80 - 0
web/src/views/xg/studentDynamics/index.vue

@@ -0,0 +1,80 @@
+<template>
+  <div class="p-2">
+    <!-- Tab页组件 -->
+    <el-tabs v-model="activeTab" @tab-click="handleTabClick" class="mb-4">
+      <!-- 调宿/退宿信息Tab页 -->
+      <el-tab-pane label="调宿/退宿信息" name="dormApplication">
+        <DormApplicationTab v-if="activeTab === 'dormApplication'" ref="dormApplicationTabRef" />
+      </el-tab-pane>
+
+      <!-- 假期留宿信息Tab页 -->
+      <el-tab-pane label="假期留宿信息" name="holidayStay">
+        <HolidayStayTab v-if="activeTab === 'holidayStay'" ref="holidayStayTabRef" />
+      </el-tab-pane>
+
+      <!-- 请假信息Tab页 -->
+      <el-tab-pane label="请假信息" name="studentLeave">
+        <StudentLeaveTab v-if="activeTab === 'studentLeave'" ref="studentLeaveTabRef" />
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script setup lang="ts" name="StudentDynamics">
+import { ref, onMounted } from 'vue';
+import DormApplicationTab from './components/DormApplicationTab.vue';
+import HolidayStayTab from './components/HolidayStayTab.vue';
+import StudentLeaveTab from './components/StudentLeaveTab.vue';
+
+// Tab页状态
+const activeTab = ref('dormApplication');
+
+// 子组件引用
+const dormApplicationTabRef = ref();
+const holidayStayTabRef = ref();
+const studentLeaveTabRef = ref();
+
+/**
+ * Tab页切换处理
+ */
+const handleTabClick = (tab: any) => {
+  console.log('切换到Tab页:', tab.name);
+  // 可以在这里添加Tab切换的逻辑
+};
+
+/**
+ * 页面初始化
+ */
+onMounted(() => {
+  console.log('学生动向信息管理页面已加载');
+});
+</script>
+
+<style scoped>
+.p-2 {
+  padding: 8px;
+}
+
+.mb-4 {
+  margin-bottom: 16px;
+}
+
+/* Tab页样式优化 */
+:deep(.el-tabs__header) {
+  margin-bottom: 20px;
+}
+
+:deep(.el-tabs__item) {
+  font-size: 14px;
+  font-weight: 500;
+}
+
+:deep(.el-tabs__item.is-active) {
+  /* color: #409eff; */
+  font-weight: 600;
+}
+
+:deep(.el-tabs__content) {
+  padding: 0;
+}
+</style>