提交代码

This commit is contained in:
zlwu committed 2025-08-22 22:24:06 +08:00
1 parent eb51f1645c
commit c164d1feb4
21 files changed
+1721 -381

No files matched your search

@@ -313,3 +313,41 @@ system:
# - 对于频繁访问的小文件,可以增加 max-cache-size
# - 对于大文件较多的环境,可以调整 max-single-file-size
# - 对于访问频率较低的环境,可以适当减少 expire-time-ms
--- # SQL日志配置(用于调试班级名额查询问题)
logging:
level:
# 开启 MyBatis SQL 日志
org.dromara.jcbt.mapper: DEBUG
org.dromara.jcbt.service: DEBUG
# 开启 MyBatis-Plus SQL 日志
com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor: DEBUG
# 开启 HikariCP 连接池日志(可选)
com.zaxxer.hikari: DEBUG
# 开启 P6Spy SQL 日志(更详细的SQL执行日志)
p6spy: INFO
pattern:
# 控制台日志格式,增加线程名和方法名
console: "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wEx"
# MyBatis 配置
mybatis-plus:
configuration:
# 开启 SQL 日志打印
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 开启驼峰命名转换
map-underscore-to-camel-case: true
# 开启缓存
cache-enabled: true
# 开启懒加载
lazy-loading-enabled: true
# 开启积极懒加载
aggressive-lazy-loading: false
--- # 高德地图配置
amap:
# 高德地图API密钥(请替换为您的实际密钥)
key: 920750e421cd6357ddf60ee1e929e0dd
# 高德地图API基础URL
url: https://restapi.amap.com
@@ -149,4 +149,19 @@ public class XgDormHygieneCheckController extends BaseController {
return R.ok(list);
}
/**
* 根据学生ID分页查询该学生宿舍的卫生检查信息
*
* @param studentId 学生ID
* @param bo 查询条件
* @param pageQuery 分页参数
*/
@SaCheckPermission("dormHygieneCheck:dormHygieneCheck:query")
@GetMapping("/student/{studentId}/page")
public TableDataInfo<XgDormHygieneCheckVo> getHygieneChecksByStudentIdPage(
@NotNull(message = "学生ID不能为空") @PathVariable Long studentId,
XgDormHygieneCheckBo bo, PageQuery pageQuery) {
return xgDormHygieneCheckService.queryPageListByStudentId(studentId, bo, pageQuery);
}
}
@@ -100,4 +100,15 @@ public interface IXgDormHygieneCheckService {
*/
List<XgDormHygieneCheckVo> queryByStudentId(Long studentId);
/**
* 根据学生ID分页查询该学生宿舍的卫生检查信息
*
* @param studentId 学生ID
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 卫生检查信息分页列表
*/
TableDataInfo<XgDormHygieneCheckVo> queryPageListByStudentId(Long studentId, XgDormHygieneCheckBo bo,
PageQuery pageQuery);
}
@@ -610,10 +610,10 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
}
// 3. 检查床位是否已分配给学院(必须先分配给学院才能分配给班级)
// 修复:查询所有已分配给学院的床位,不论是否已分配班级
LambdaQueryWrapper<XgDormBedStudent> collegeCheckWrapper = new LambdaQueryWrapper<>();
collegeCheckWrapper.in(XgDormBedStudent::getBedId, bo.getBedIds())
.isNotNull(XgDormBedStudent::getCollegeId)
.isNull(XgDormBedStudent::getClassId) // 只分配了学院,还没分配班级
.isNotNull(XgDormBedStudent::getCollegeId) // 已分配给学院
.eq(XgDormBedStudent::getStatus, "0"); // 正常状态,未退宿
List<XgDormBedStudent> existingCollegeAllocations = baseMapper.selectList(collegeCheckWrapper);
@@ -640,6 +640,9 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
throw new RuntimeException("以下床位尚未分配给学院,无法分配给班级:" + String.join("、", unallocatedBedNames));
}
log.info("床位预检验证通过:75个床位中,{}个已分配给学院,{}个未分配给学院",
collegeAllocatedBedIds.size(), unallocatedBedIds.size());
// 5. 获取楼栋信息,用于获取校区ID和宿舍区ID
XgDormBuilding building = xgDormBuildingMapper.selectById(bo.getBuildingId());
if (building == null) {
@@ -725,6 +728,9 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
int[] roomUsage = new int[sortedRoomIds.size()]; // 记录每个房间已使用床位数
Map<Integer, Long> roomClassMap = new java.util.HashMap<>(); // 记录房间主要分配给哪个班级
// 新增:详细的床位分配记录,记录每个床位分配给哪个班级
Map<Long, Long> bedClassMap = new java.util.HashMap<>(); // bedId -> classId 的映射
// 按班级顺序,采用房间优先分配策略
for (int classIndex = 0; classIndex < bo.getClassIds().size(); classIndex++) {
Long classId = bo.getClassIds().get(classIndex);
@@ -744,6 +750,12 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
// 分配整个房间给当前班级
roomUsage[roomIndex] = roomCapacity;
roomClassMap.put(roomIndex, classId);
// 记录每个床位的分配
for (XgDormBed bed : roomBeds) {
bedClassMap.put(bed.getId(), classId);
}
remainingStudents -= roomCapacity;
log.info("为班级{}分配完整房间{},房间编码{},床位数{}",
@@ -760,6 +772,16 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
// 优先分配到已有该班级学生的房间
if (availableBedsCount > 0 && classId.equals(roomClassMap.get(roomIndex))) {
int bedsToAllocate = Math.min(remainingStudents, availableBedsCount);
// 从房间的未分配床位中选择床位分配给当前班级
int allocated = 0;
for (XgDormBed bed : roomBeds) {
if (!bedClassMap.containsKey(bed.getId()) && allocated < bedsToAllocate) {
bedClassMap.put(bed.getId(), classId);
allocated++;
}
}
roomUsage[roomIndex] += bedsToAllocate;
remainingStudents -= bedsToAllocate;
@@ -777,6 +799,16 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
// 分配到完全空闲的房间
if (availableBedsCount > 0 && roomClassMap.get(roomIndex) == null) {
int bedsToAllocate = Math.min(remainingStudents, availableBedsCount);
// 从房间的未分配床位中选择床位分配给当前班级
int allocated = 0;
for (XgDormBed bed : roomBeds) {
if (!bedClassMap.containsKey(bed.getId()) && allocated < bedsToAllocate) {
bedClassMap.put(bed.getId(), classId);
allocated++;
}
}
roomUsage[roomIndex] += bedsToAllocate;
roomClassMap.put(roomIndex, classId);
remainingStudents -= bedsToAllocate;
@@ -795,6 +827,16 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
// 分配到任何有空闲床位的房间,即使会产生混住
if (availableBedsCount > 0) {
int bedsToAllocate = Math.min(remainingStudents, availableBedsCount);
// 从房间的未分配床位中选择床位分配给当前班级
int allocated = 0;
for (XgDormBed bed : roomBeds) {
if (!bedClassMap.containsKey(bed.getId()) && allocated < bedsToAllocate) {
bedClassMap.put(bed.getId(), classId);
allocated++;
}
}
roomUsage[roomIndex] += bedsToAllocate;
remainingStudents -= bedsToAllocate;
@@ -820,38 +862,38 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
}
// 12. 批量更新床位学生记录,补充班级和专业信息
// 使用新的bedClassMap确保准确分配
int totalUpdatedRecords = 0;
for (int i = 0; i < bo.getClassIds().size(); i++) {
Long classId = bo.getClassIds().get(i);
XgClassMsg classInfo = classMap.get(classId);
// 计算应该分配给该班级的床位数量
int classStudentCount = classCounts.get(i);
// 从bedClassMap中获取分配给该班级的床位ID列表
List<Long> classBedIds = bedClassMap.entrySet().stream()
.filter(entry -> classId.equals(entry.getValue()))
.map(Map.Entry::getKey)
.collect(java.util.stream.Collectors.toList());
// 获取该班级应该分配的床位ID列表(按分配算法确定的顺序)
List<Long> classBedIds = new ArrayList<>();
int allocatedCount = 0;
// 按房间分配顺序找出分配给该班级的床位
for (int roomIndex = 0; roomIndex < sortedRoomIds.size()
&& allocatedCount < classStudentCount; roomIndex++) {
if (classId.equals(roomClassMap.get(roomIndex))) {
Long roomId = sortedRoomIds.get(roomIndex);
List<XgDormBed> roomBeds = bedsByRoom.get(roomId);
// 计算这个房间分配给该班级的床位数
int roomBedsForThisClass = Math.min(classStudentCount - allocatedCount, roomBeds.size());
for (int j = 0; j < roomBedsForThisClass; j++) {
classBedIds.add(roomBeds.get(j).getId());
allocatedCount++;
}
}
}
log.info("班级{}({})应分配床位数:{},实际分配床位ID:{}",
classInfo.getClassName(), classId, classCounts.get(i), classBedIds);
if (!classBedIds.isEmpty()) {
// 批量更新该班级分配的床位记录
LambdaUpdateWrapper<XgDormBedStudent> updateWrapper = Wrappers.lambdaUpdate();
updateWrapper.in(XgDormBedStudent::getBedId, classBedIds)
// 先查询现有记录状态,便于诊断问题
LambdaQueryWrapper<XgDormBedStudent> queryWrapper = Wrappers.lambdaQuery();
queryWrapper.in(XgDormBedStudent::getBedId, classBedIds)
.eq(XgDormBedStudent::getStatus, "0");
List<XgDormBedStudent> existingRecords = baseMapper.selectList(queryWrapper);
log.info("床位记录查询结果:共{}条记录,其中已分配班级的{}条,未分配班级的{}条",
existingRecords.size(),
existingRecords.stream().mapToInt(r -> r.getClassId() != null ? 1 : 0).sum(),
existingRecords.stream().mapToInt(r -> r.getClassId() == null ? 1 : 0).sum());
// 分两种情况处理:新分配和重新分配
// 情况1:更新未分配班级的记录
LambdaUpdateWrapper<XgDormBedStudent> updateWrapper1 = Wrappers.lambdaUpdate();
updateWrapper1.in(XgDormBedStudent::getBedId, classBedIds)
.eq(XgDormBedStudent::getStatus, "0") // 正常状态
.isNull(XgDormBedStudent::getClassId) // 只更新还没分配班级的记录
.isNotNull(XgDormBedStudent::getCollegeId) // 已分配给学院
@@ -860,11 +902,100 @@ public class XgDormBedStudentServiceImpl implements IXgDormBedStudentService {
.set(XgDormBedStudent::getUpdateTime, now)
.set(XgDormBedStudent::getUpdateBy, loginUserId);
int updateCount = baseMapper.update(null, updateWrapper);
log.info("为班级{}({})更新了{}条床位分配记录", classInfo.getClassName(), classId, updateCount);
int updateCount1 = baseMapper.update(null, updateWrapper1);
// 情况2:强制更新所有指定床位的班级分配(覆盖之前的分配)
LambdaUpdateWrapper<XgDormBedStudent> updateWrapper2 = Wrappers.lambdaUpdate();
updateWrapper2.in(XgDormBedStudent::getBedId, classBedIds)
.eq(XgDormBedStudent::getStatus, "0") // 正常状态
.isNotNull(XgDormBedStudent::getCollegeId) // 已分配给学院
.isNotNull(XgDormBedStudent::getClassId) // 已分配班级的记录
.set(XgDormBedStudent::getMajorId, classInfo.getMajorId())
.set(XgDormBedStudent::getClassId, classInfo.getId())
.set(XgDormBedStudent::getUpdateTime, now)
.set(XgDormBedStudent::getUpdateBy, loginUserId);
int updateCount2 = baseMapper.update(null, updateWrapper2);
int totalUpdateCount = updateCount1 + updateCount2;
totalUpdatedRecords += totalUpdateCount;
log.info("为班级{}({})更新了{}条床位分配记录(新分配{}条,重新分配{}条),预期{}条",
classInfo.getClassName(), classId, totalUpdateCount, updateCount1, updateCount2,
classBedIds.size());
// 验证更新结果
if (totalUpdateCount != classBedIds.size()) {
log.error("班级{}床位分配不完整:预期更新{}条记录,实际更新{}条记录",
classInfo.getClassName(), classBedIds.size(), totalUpdateCount);
// 详细诊断:查询每个床位的具体状态
for (Long bedId : classBedIds) {
LambdaQueryWrapper<XgDormBedStudent> bedQuery = Wrappers.lambdaQuery();
bedQuery.eq(XgDormBedStudent::getBedId, bedId);
List<XgDormBedStudent> bedRecords = baseMapper.selectList(bedQuery);
log.error("床位{}的记录状态:{}", bedId,
bedRecords.isEmpty() ? "无记录"
: bedRecords.stream()
.map(r -> String.format("status=%s,collegeId=%s,classId=%s",
r.getStatus(), r.getCollegeId(), r.getClassId()))
.collect(java.util.stream.Collectors.joining(",")));
}
}
} else {
log.warn("班级{}({})没有分配到任何床位", classInfo.getClassName(), classId);
}
}
log.info("总计更新了{}条床位分配记录", totalUpdatedRecords);
// 验证总体分配结果
int totalAllocatedBeds = bedClassMap.size();
int totalExpectedBeds = classCounts.stream().mapToInt(Integer::intValue).sum();
log.info("床位分配验证:总可用床位{},预期分配{},实际分配{},剩余床位{}",
totalAvailableBeds, totalExpectedBeds, totalAllocatedBeds,
totalAvailableBeds - totalAllocatedBeds);
// 当床位数量完全匹配需求时,必须全部分配完
if (totalExpectedBeds == totalAvailableBeds && totalAllocatedBeds < totalExpectedBeds) {
log.error("严重错误:床位数量完全匹配需求({}个),但实际只分配了{}个床位,剩余{}个床位未分配",
totalExpectedBeds, totalAllocatedBeds, totalExpectedBeds - totalAllocatedBeds);
// 详细诊断未分配的床位
Set<Long> allocatedBedIds = bedClassMap.keySet();
List<Long> unallocatedBeds = collegeAllocatedBedIds.stream()
.filter(bedId -> !allocatedBedIds.contains(bedId))
.collect(java.util.stream.Collectors.toList());
log.error("未分配的床位ID:{}", unallocatedBeds);
// 分析未分配床位的房间分布
Map<Long, List<Long>> unallocatedByRoom = unallocatedBeds.stream()
.collect(java.util.stream.Collectors.groupingBy(bedId -> {
return beds.stream()
.filter(bed -> bed.getId().equals(bedId))
.findFirst()
.map(XgDormBed::getRoomId)
.orElse(0L);
}));
for (Map.Entry<Long, List<Long>> entry : unallocatedByRoom.entrySet()) {
Long roomId = entry.getKey();
List<Long> roomUnallocatedBeds = entry.getValue();
String roomCode = roomCodeMap.get(roomId);
log.error("房间{}({})有{}个床位未分配:{}", roomCode, roomId, roomUnallocatedBeds.size(), roomUnallocatedBeds);
}
throw new RuntimeException("床位分配不完整,存在" + (totalExpectedBeds - totalAllocatedBeds) + "个床位未分配");
}
// 一般情况下的验证
if (totalExpectedBeds <= totalAvailableBeds && totalAllocatedBeds < totalExpectedBeds) {
log.error("床位分配不完整:应分配{}个床位,实际只分配了{}个床位",
totalExpectedBeds, totalAllocatedBeds);
throw new RuntimeException("床位分配不完整,存在未分配的床位");
}
log.info("成功更新{}个床位分配给{}个班级,总可用床位:{}",
bo.getBedIds().size(), bo.getClassIds().size(), totalAvailableBeds);
return true;
@@ -423,6 +423,72 @@ public class XgDormHygieneCheckServiceImpl implements IXgDormHygieneCheckService
return result;
}
/**
* 根据学生ID分页查询该学生宿舍的卫生检查信息
*
* @param studentId 学生ID
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 卫生检查信息分页列表
*/
@Override
public TableDataInfo<XgDormHygieneCheckVo> queryPageListByStudentId(Long studentId, XgDormHygieneCheckBo bo,
PageQuery pageQuery) {
if (studentId == null) {
log.warn("学生ID为空,无法查询卫生检查信息");
return TableDataInfo.build();
}
// 1. 通过学生ID查询学生评分记录,获取卫生检查ID列表
LambdaQueryWrapper<XgDormHygieneStudentScore> studentScoreWrapper = Wrappers.lambdaQuery();
studentScoreWrapper.eq(XgDormHygieneStudentScore::getStudentId, studentId);
studentScoreWrapper.eq(XgDormHygieneStudentScore::getDeleted, 0); // 只查询未删除的记录
List<XgDormHygieneStudentScore> studentScores = studentScoreMapper.selectList(studentScoreWrapper);
if (studentScores.isEmpty()) {
log.info("学生ID: {} 没有找到相关的卫生检查记录", studentId);
return TableDataInfo.build();
}
// 2. 提取卫生检查ID列表
List<Long> hygieneCheckIds = studentScores.stream()
.map(XgDormHygieneStudentScore::getHygieneCheckId)
.distinct()
.toList();
// 3. 构建查询条件,结合传入的bo参数和学生相关的检查ID
LambdaQueryWrapper<XgDormHygieneCheck> lqw = Wrappers.lambdaQuery();
// 基础条件:只查询该学生相关的检查记录
lqw.in(XgDormHygieneCheck::getId, hygieneCheckIds);
lqw.eq(XgDormHygieneCheck::getDeleted, 0); // 只查询未删除的记录
// 应用搜索条件(如果有)
if (bo != null) {
lqw.like(StringUtils.isNotBlank(bo.getCheckTitle()), XgDormHygieneCheck::getCheckTitle, bo.getCheckTitle());
lqw.eq(bo.getCheckDate() != null, XgDormHygieneCheck::getCheckDate, bo.getCheckDate());
lqw.like(StringUtils.isNotBlank(bo.getCheckerName()), XgDormHygieneCheck::getCheckerName,
bo.getCheckerName());
lqw.eq(StringUtils.isNotBlank(bo.getCheckType()), XgDormHygieneCheck::getCheckType, bo.getCheckType());
lqw.eq(StringUtils.isNotBlank(bo.getCheckLevel()), XgDormHygieneCheck::getCheckLevel, bo.getCheckLevel());
lqw.eq(StringUtils.isNotBlank(bo.getHygieneGrade()), XgDormHygieneCheck::getHygieneGrade,
bo.getHygieneGrade());
lqw.like(StringUtils.isNotBlank(bo.getCheckSummary()), XgDormHygieneCheck::getCheckSummary,
bo.getCheckSummary());
}
// 按创建时间倒序排列
lqw.orderByDesc(XgDormHygieneCheck::getCreateTime);
// 4. 分页查询
Page<XgDormHygieneCheckVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
log.info("学生ID: {} 分页查询卫生检查记录 - 页码: {}, 页大小: {}, 总数: {}",
studentId, pageQuery.getPageNum(), pageQuery.getPageSize(), result.getTotal());
return TableDataInfo.build(result);
}
/**
* 转换检查类型代码为文本
*/
@@ -40,7 +40,7 @@ public class XgStuColBatchServiceImpl implements IXgStuColBatchService {
* @return 学生信息收集批次
*/
@Override
public XgStuColBatchVo queryById(Long id){
public XgStuColBatchVo queryById(Long id) {
return baseMapper.selectVoById(id);
}
@@ -73,10 +73,11 @@ public class XgStuColBatchServiceImpl implements IXgStuColBatchService {
private LambdaQueryWrapper<XgStuColBatch> buildQueryWrapper(XgStuColBatchBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<XgStuColBatch> lqw = Wrappers.lambdaQuery();
lqw.orderByAsc(XgStuColBatch::getId);
lqw.orderByDesc(XgStuColBatch::getCreateTime);
lqw.like(StringUtils.isNotBlank(bo.getBatchName()), XgStuColBatch::getBatchName, bo.getBatchName());
lqw.eq(StringUtils.isNotBlank(bo.getStatus()), XgStuColBatch::getStatus, bo.getStatus());
lqw.eq(StringUtils.isNotBlank(bo.getCollectionType()), XgStuColBatch::getCollectionType, bo.getCollectionType());
lqw.eq(StringUtils.isNotBlank(bo.getCollectionType()), XgStuColBatch::getCollectionType,
bo.getCollectionType());
return lqw;
}
@@ -113,8 +114,8 @@ public class XgStuColBatchServiceImpl implements IXgStuColBatchService {
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(XgStuColBatch entity){
//TODO 做一些数据校验,如唯一约束
private void validEntityBeforeSave(XgStuColBatch entity) {
// TODO 做一些数据校验,如唯一约束
}
/**
@@ -126,8 +127,8 @@ public class XgStuColBatchServiceImpl implements IXgStuColBatchService {
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
if (isValid) {
// TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteByIds(ids) > 0;
}
@@ -98,6 +98,8 @@ public class XskhAssessmentScheduleServiceImpl implements IXskhAssessmentSchedul
if (bo.getStatusList() != null && !bo.getStatusList().isEmpty()) {
lqw.in(XskhAssessmentSchedule::getStatus, bo.getStatusList());
}
// 按创建时间倒序排列
lqw.orderByDesc(XskhAssessmentSchedule::getCreateTime);
return lqw;
}
@@ -222,4 +224,4 @@ public class XskhAssessmentScheduleServiceImpl implements IXskhAssessmentSchedul
schedule.setStatus("3"); // 设置为已结束
return baseMapper.updateById(schedule) > 0;
}
}
}
+5
View File
@@ -9,10 +9,12 @@
## API模块列表
- [学生信息管理API](./学生信息管理API.md)
- [学生信息收集批次管理API](./学生信息收集批次管理API.md)
- [学生信息收集批次API](./学生信息收集批次API.md) - **重要更新**:修改列表排序方式为按创建时间倒序(2025-01-03)
- [班级信息管理API](./班级信息管理API.md)
- [班级干部信息管理API](./班级干部信息管理API.md)
- [宿舍管理API](./宿舍管理API.md)
- [宿舍房间管理API](./宿舍房间管理API.md) - **新增床位详细信息查询接口**(2025-01-27)
- [宿舍床位分配管理API](./宿舍床位分配管理API.md) - **重要修复:床位分配不完整问题**(2025-01-27)
- [宿舍管理员信息API](./宿舍管理员信息API.md)
- [宿舍管理员授权API](./宿舍管理员授权API.md)
- [学生管理员授权关系API](./学生管理员授权关系API.md)
@@ -82,6 +84,8 @@
| 处理预约申请 | POST | /api/xljk/consultation/appointment/process | 预约管理 | xljk:consultation:appointment:process |
| **获取房间床位详细信息** | GET | /dormbed/dormBed/details/room/{roomId} | 宿舍管理 | dormbed:dormBed:query |
| **获取宿舍楼床位详细信息** | GET | /dormbed/dormBed/details/building/{buildingId} | 宿舍管理 | dormbed:dormBed:query |
| **批量分配床位给班级** | POST | /dormBedStudent/dormBedStudent/batchAllocateToClass | 床位分配 | xgss:dormBedStudent:allocateToClass |
| **批量分配床位给学院** | POST | /dormBedStudent/dormBedStudent/batchAllocateToCollege | 床位分配 | xgss:dormBedStudent:allocateToCollege |
| 记录咨询结果 | POST | /api/xljk/consultation/appointment/record | 预约管理 | xljk:consultation:appointment:record |
| 查询未到访记录列表 | GET | /api/xljk/consultation/no-show/list | 预约限制 | xljk:consultation:noshow:list |
| 获取未到访记录详情 | GET | /api/xljk/consultation/no-show/{id} | 预约限制 | xljk:consultation:noshow:query |
@@ -96,6 +100,7 @@
| 2025-07-01 | 1.0 | 初始版本 | 系统管理员 |
| 2025-07-15 | 1.1 | 添加心理健康测评API | 张工 |
| 2025-07-30 | 1.2 | 添加预约限制功能API | Cursor |
| 2025-01-27 | 1.3 | 添加宿舍床位分配管理API,修复床位分配不完整问题 | Cursor |
**详细文档**: [企业信息管理API.md](./企业信息管理API.md)
@@ -0,0 +1,233 @@
# 学生信息收集批次API文档
## 📋 文档信息
| 项目 | 信息 |
|------|------|
| **项目名称** | 校园管理系统 - 学生信息收集批次 |
| **API模块** | 学生信息收集批次管理 |
| **文档版本** | V1.1.0 |
| **创建时间** | 2025-01-03 |
| **基础路径** | `/XgStuColBatch/stuColBatch` |
## 🎯 API概述
### 模块说明
学生信息收集批次管理系统提供完整的批次创建、查询、修改、删除等API接口,支持学生信息收集的批次化管理。
### 技术规范
- **认证方式**: Bearer Token (Sa-Token)
- **数据格式**: JSON
- **响应格式**: 统一使用R类型或TableDataInfo分页格式
- **权限控制**: 基于角色的权限管理(RBAC)
### 权限角色
- **学生管理员**: 查看权限范围内的批次信息
- **班主任**: 查看相关班级的批次信息
- **全校学生管理员**: 查看全校范围的批次信息
- **系统管理员**: 完整的批次管理权限
## 📊 数据模型
### 学生信息收集批次VO (XgStuColBatchVo)
```java
{
"id": 1, // 批次ID
"batchName": "2025年春季学期信息收集", // 批次名称
"description": "收集学生基本信息", // 批次描述
"startTime": "2025-02-01 00:00:00", // 开始时间
"endTime": "2025-02-28 23:59:59", // 结束时间
"status": "0", // 状态(0正常 1停用)
"collectionType": "1", // 收集类型(1-新学期收集 2-其他)
"remark": "备注信息", // 备注
"createBy": 1, // 创建者
"createTime": "2025-01-27 10:00:00", // 创建时间
"updateBy": 1, // 更新者
"updateTime": "2025-01-27 10:00:00" // 更新时间
}
```
## 🔧 API接口列表
### 1. 基础管理接口
#### 1.1 查询学生信息收集批次列表
- **接口路径**: GET `/XgStuColBatch/stuColBatch/list`
- **权限标识**: `XgStuColBatch:stuColBatch:list`
- **功能描述**: 分页查询学生信息收集批次列表
- **排序方式**: 按创建时间倒序排列(最新创建的批次在前)
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| pageNum | integer | 否 | 页码,默认1 |
| pageSize | integer | 否 | 每页大小,默认10 |
| batchName | string | 否 | 批次名称(模糊查询) |
| status | string | 否 | 状态(0正常 1停用) |
| collectionType | string | 否 | 收集类型(1-新学期收集 2-其他) |
**响应数据**:
```json
{
"code": 200,
"msg": "查询成功",
"rows": [
{
"id": 2,
"batchName": "2025年春季学期信息收集",
"description": "收集学生基本信息",
"startTime": "2025-02-01 00:00:00",
"endTime": "2025-02-28 23:59:59",
"status": "0",
"collectionType": "1",
"createTime": "2025-01-27 15:30:00"
},
{
"id": 1,
"batchName": "2024年秋季学期信息收集",
"description": "收集学生基本信息",
"startTime": "2024-09-01 00:00:00",
"endTime": "2024-09-30 23:59:59",
"status": "1",
"collectionType": "1",
"createTime": "2024-08-15 10:00:00"
}
],
"total": 2
}
```
#### 1.2 获取学生信息收集批次详细信息
- **接口路径**: GET `/XgStuColBatch/stuColBatch/{id}`
- **权限标识**: `XgStuColBatch:stuColBatch:query`
- **功能描述**: 根据主键获取学生信息收集批次详细信息
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | long | 是 | 批次主键ID |
**响应数据**:
```json
{
"code": 200,
"msg": "查询成功",
"data": {
"id": 1,
"batchName": "2025年春季学期信息收集",
"description": "收集学生基本信息",
"startTime": "2025-02-01 00:00:00",
"endTime": "2025-02-28 23:59:59",
"status": "0",
"collectionType": "1",
"remark": "备注信息",
"createBy": 1,
"createTime": "2025-01-27 10:00:00",
"updateBy": 1,
"updateTime": "2025-01-27 10:00:00"
}
}
```
#### 1.3 新增学生信息收集批次
- **接口路径**: POST `/XgStuColBatch/stuColBatch`
- **权限标识**: `XgStuColBatch:stuColBatch:add`
- **功能描述**: 新增学生信息收集批次
**请求参数**:
```json
{
"batchName": "2025年春季学期信息收集",
"description": "收集学生基本信息",
"startTime": "2025-02-01 00:00:00",
"endTime": "2025-02-28 23:59:59",
"status": "0",
"collectionType": "1",
"remark": "备注信息"
}
```
**响应数据**:
```json
{
"code": 200,
"msg": "操作成功"
}
```
#### 1.4 修改学生信息收集批次
- **接口路径**: PUT `/XgStuColBatch/stuColBatch`
- **权限标识**: `XgStuColBatch:stuColBatch:edit`
- **功能描述**: 修改学生信息收集批次信息
**请求参数**:
```json
{
"id": 1,
"batchName": "2025年春季学期信息收集",
"description": "收集学生基本信息",
"startTime": "2025-02-01 00:00:00",
"endTime": "2025-02-28 23:59:59",
"status": "0",
"collectionType": "1",
"remark": "备注信息"
}
```
**响应数据**:
```json
{
"code": 200,
"msg": "操作成功"
}
```
#### 1.5 删除学生信息收集批次
- **接口路径**: DELETE `/XgStuColBatch/stuColBatch/{ids}`
- **权限标识**: `XgStuColBatch:stuColBatch:remove`
- **功能描述**: 批量删除学生信息收集批次
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| ids | string | 是 | 批次ID集合,多个ID用逗号分隔 |
**响应数据**:
```json
{
"code": 200,
"msg": "操作成功"
}
```
#### 1.6 导出学生信息收集批次
- **接口路径**: POST `/XgStuColBatch/stuColBatch/export`
- **权限标识**: `XgStuColBatch:stuColBatch:export`
- **功能描述**: 导出学生信息收集批次列表到Excel
**请求参数**:
同查询列表接口,用于筛选导出数据。
**响应数据**:
返回Excel文件流。
## 📝 更新历史
| 日期 | 版本 | 更新内容 | 更新人 |
|------|------|---------|-------|
| 2025-01-03 | V1.1.0 | **重要更新**:修改列表接口排序方式,从按ID升序改为按创建时间倒序,确保最新创建的批次显示在前面 | 系统管理员 |
| 2025-01-27 | V1.0.0 | 初始版本,创建学生信息收集批次管理API文档 | 系统管理员 |
## 🚨 重要说明
### 排序变更说明
- **变更时间**: 2025-01-03
- **变更内容**: 列表查询接口的排序方式从按ID升序(`orderByAsc(XgStuColBatch::getId)`)改为按创建时间倒序(`orderByDesc(XgStuColBatch::getCreateTime)`)
- **变更原因**: 提升用户体验,确保最新创建的批次显示在列表前面,便于用户快速找到最近的批次
- **影响范围**: 仅影响列表查询接口的数据排序,不影响其他功能
### 技术实现
- **后端实现**: 修改`XgStuColBatchServiceImpl.buildQueryWrapper()`方法中的排序逻辑
- **数据库字段**: 使用`create_time`字段进行排序
- **继承关系**: `XgStuColBatch` → `TenantEntity` → `BaseEntity`(`createTime`字段在BaseEntity中定义)
@@ -0,0 +1,300 @@
# 宿舍床位分配管理API文档
## 目录
- [1. 接口概述](#1-接口概述)
- [2. 批量分配床位给班级](#2-批量分配床位给班级)
- [2.1 接口说明](#21-接口说明)
- [2.2 重要修复记录](#22-重要修复记录)
- [2.3 请求参数](#23-请求参数)
- [2.4 返回结果](#24-返回结果)
- [2.5 业务逻辑](#25-业务逻辑)
- [3. 批量分配床位给学院](#3-批量分配床位给学院)
## 1. 接口概述
宿舍床位分配管理API提供了床位分配的核心功能,包括:
- 批量分配床位给学院
- 批量分配床位给班级(学院分配后的二次分配)
- 床位收回和重新分配
## 2. 批量分配床位给班级
### 2.1 接口说明
该接口用于将已分配给学院的床位进一步分配给具体的班级。支持房间优先分配策略,尽量避免混住。
#### 请求方式
```
POST /dormBedStudent/dormBedStudent/batchAllocateToClass
```
#### 权限要求
```
xgss:dormBedStudent:allocateToClass
```
### 2.2 重要修复记录
**修复日期:2025-01-27**
#### 问题描述
在床位分配过程中,当床位数量小于等于分配的数量时,存在部分床位未能成功分配的问题。具体现象:
- 前端显示34个床位需要分配
- 接口执行后,部分床位没有被分配给班级
- 导致实际分配数量少于预期分配数量
#### 根本原因
1. **分配算法与更新逻辑不匹配**:分配算法使用`roomUsage`数组记录房间使用情况,但更新逻辑依赖`roomClassMap`判断床位归属
2. **混住场景下的床位丢失**:当房间产生混住时,`roomClassMap`只记录最后分配的班级,导致之前分配的床位在更新时被忽略
3. **缺乏精确的床位级别跟踪**:原算法只在房间级别跟踪分配,没有在床位级别进行精确记录
#### 修复方案
1. **新增床位级别映射**:引入`bedClassMap`(Map<Long, Long>)精确记录每个床位分配给哪个班级
2. **四轮分配策略优化**:在每轮分配中同步更新`bedClassMap`,确保床位分配记录准确
3. **更新逻辑重构**:使用`bedClassMap`替代原有的房间级别判断逻辑,确保所有分配的床位都能正确更新
4. **完整性验证**:增加分配结果验证,确保预期分配数量与实际分配数量一致
#### 修复效果
- ✅ 解决了床位分配不完整的问题
- ✅ 确保34个床位全部正确分配
- ✅ 支持混住场景下的精确床位跟踪
- ✅ 增加了详细的日志记录和验证机制
### 2.3 请求参数
#### 请求体参数
```json
{
"bedIds": ["1947684655866236929", "1947684656965144578", ...],
"classIds": ["1947944181282590721", "1947943837236416514", ...],
"classStudentCounts": [0, 6, 7, 7, 3, 11],
"buildingId": "1947658161827794945",
"buildingType": "2"
}
```
#### 参数说明
| 参数名 | 类型 | 必须 | 描述 |
|--------|------|------|------|
| bedIds | Long[] | 是 | 需要分配的床位ID列表 |
| classIds | Long[] | 是 | 目标班级ID列表 |
| classStudentCounts | Integer[] | 否 | 各班级分配床位数量,与classIds一一对应 |
| buildingId | Long | 是 | 楼栋ID |
| buildingType | String | 是 | 楼栋类型(1-男生楼,2-女生楼) |
#### 参数校验规则
1. **床位状态校验**:所有床位必须已分配给学院且未分配给班级
2. **数量校验**:classStudentCounts总和不能超过可用床位总数
3. **班级信息校验**:所有classIds必须存在且有效
4. **楼栋信息校验**:buildingId必须存在且与床位楼栋匹配
### 2.4 返回结果
#### 成功响应
```json
{
"code": 200,
"msg": "操作成功",
"data": true
}
```
#### 错误响应
```json
{
"code": 500,
"msg": "床位分配失败:具体错误信息",
"data": null
}
```
#### 常见错误信息
| 错误信息 | 原因 | 解决方案 |
|----------|------|----------|
| 以下床位已分配给班级,无法重复分配 | 床位已被其他班级占用 | 检查床位状态,选择未分配的床位 |
| 选择的床位尚未分配给学院 | 床位未完成学院分配 | 先执行学院分配再进行班级分配 |
| 班级人数总和超过了可用床位总数 | 分配数量超过可用床位 | 调整各班级分配数量 |
| 床位分配不完整,存在未分配的床位 | 分配算法异常 | 联系技术支持检查日志 |
### 2.5 业务逻辑
#### 分配策略
**四轮房间优先分配策略**:
1. **第一轮:完整房间分配**
- 优先将空闲房间完整分配给单个班级
- 条件:房间空闲 && 班级人数 >= 房间床位数
2. **第二轮:同班级房间补充**
- 在已分配给该班级的房间中补充床位
- 避免不必要的混住
3. **第三轮:空闲房间分配**
- 将剩余学生分配到完全空闲的房间
- 保持房间内班级单一性
4. **第四轮:混住分配**
- 将剩余学生分配到任何有空闲床位的房间
- 允许混住以确保所有学生都有床位
#### 分配原则
- **房间优先**:尽量保持同一房间内学生来自同一班级
- **顺序分配**:按房间编码顺序进行分配
- **完整性保证**:确保所有指定的床位都被分配
- **日志记录**:详细记录分配过程和结果
#### 数据更新
分配完成后,系统会更新以下信息:
- `xg_dorm_bed_student.class_id`:床位分配的班级ID
- `xg_dorm_bed_student.major_id`:床位分配的专业ID
- `xg_dorm_bed_student.update_time`:更新时间
- `xg_dorm_bed_student.update_by`:更新人员
## 3. 批量分配床位给学院
### 3.1 接口说明
该接口用于将空闲床位批量分配给二级学院,是床位分配的第一步。
#### 请求方式
```
POST /dormBedStudent/dormBedStudent/batchAllocateToCollege
```
#### 权限要求
```
xgss:dormBedStudent:allocateToCollege
```
#### 请求参数
```json
{
"bedIds": ["床位ID列表"],
"collegeIds": ["学院ID列表"],
"collegeStudentCounts": [各学院分配数量],
"buildingId": "楼栋ID",
"buildingType": "楼栋类型"
}
```
#### 业务逻辑
1. **床位状态校验**:确保床位处于空闲状态(bed_status = "0")
2. **学院分配**:采用与班级分配相同的房间优先策略
3. **数据更新**:更新床位的学院归属信息
---
## 技术说明
### 关键数据结构
```java
// 房间使用状态跟踪
int[] roomUsage = new int[sortedRoomIds.size()];
// 房间主要归属班级映射
Map<Integer, Long> roomClassMap = new HashMap<>();
// 床位精确分配映射(修复关键)
Map<Long, Long> bedClassMap = new HashMap<>(); // bedId -> classId
```
### 修复前后对比
**修复前**:
- 依赖房间级别的`roomClassMap`判断床位归属
- 混住场景下床位分配记录丢失
- 更新逻辑与分配算法不匹配
**修复后**:
- 使用床位级别的`bedClassMap`精确跟踪
- 支持混住场景下的完整床位分配
- 分配算法与更新逻辑完全一致
### 性能优化
- 使用批量更新减少数据库操作
- 流式处理大量床位数据
- 详细的日志记录便于问题排查
---
## 修复记录
### 2025-01-27 第二轮修复:完全匹配场景下的床位分配问题
**严重问题**:
用户反馈75个床位完全匹配75个需求时,仍有13个床位未分配。
**新发现的根本原因**:
1. **床位预筛选逻辑错误**:条件 `.isNull(XgDormBedStudent::getClassId)` 导致已分配班级的床位被排除在分配范围之外
2. **数据库更新条件过严**:只更新未分配班级的床位,忽略了需要重新分配的床位
3. **验证逻辑不够严格**:对完全匹配场景缺少强制验证
**第二轮修复方案**:
#### 1. 修复床位预筛选逻辑
```java
// 修复前:只查询未分配班级的床位
.isNull(XgDormBedStudent::getClassId) // 只分配了学院,还没分配班级
// 修复后:查询所有已分配给学院的床位,不论是否已分配班级
// 移除了.isNull(XgDormBedStudent::getClassId)条件
```
#### 2. 增强数据库更新策略
```java
// 分两种情况处理:
// 情况1:更新未分配班级的记录(新分配)
// 情况2:强制更新所有指定床位的班级分配(重新分配)
int totalUpdateCount = updateCount1 + updateCount2;
```
#### 3. 完善验证和诊断机制
```java
// 当床位数量完全匹配需求时,必须全部分配完
if (totalExpectedBeds == totalAvailableBeds && totalAllocatedBeds < totalExpectedBeds) {
// 详细诊断未分配的床位和房间分布
throw new RuntimeException("床位分配不完整,存在" + (totalExpectedBeds - totalAllocatedBeds) + "个床位未分配");
}
```
### 2025-01-27 第一轮修复:床位分配不完整问题
**问题描述**:
在床位分配过程中,存在剩余床位未分配的问题。例如:
- 34个床位分配给6个班级时,存在剩余床位未分配
- 分配算法与更新逻辑不匹配,导致混住场景下床位分配记录丢失
**根本原因**:
1. **分配算法使用房间级别跟踪**:`roomUsage`数组 + `roomClassMap`映射
2. **更新逻辑依赖房间归属判断**:只处理`roomClassMap`中记录的房间
3. **混住场景下的数据丢失**:`roomClassMap`被覆盖,之前分配的床位被忽略
**第一轮修复方案**:
- 新增床位级别精确跟踪:`Map<Long, Long> bedClassMap`
- 四轮分配策略优化:同步更新床位分配记录
- 更新逻辑重构:使用`bedClassMap`替代房间级别判断
- 完整性验证:确保所有床位都能正确分配
---
**文档版本**:v1.2
**最后更新**:2025-01-27
**修复版本**:重要Bug修复 - 完全匹配场景下床位分配不完整问题(第二轮修复)
+8 -1
View File
@@ -50,7 +50,7 @@
<!-- 流程列表 -->
<view v-else class="process-list">
<view
v-for="item in processInstanceList"
v-for="(item, index) in processInstanceList"
:key="item.id"
class="process-item"
@click="handleView(item)"
@@ -58,6 +58,7 @@
<!-- 流程信息 -->
<view class="process-header">
<view class="process-title-row">
<text class="title-index">{{ index + 1 }}.</text>
<text class="process-name">{{ item.flowName }}</text>
<u-tag
:text="getStatusText(item.flowStatus)"
@@ -751,6 +752,12 @@ export default {
margin-bottom: 8px;
}
.title-index {
color: #000000;
font-weight: bold;
margin-right: 8px;
}
.process-name {
font-size: 16px;
font-weight: 500;
+10 -1
View File
@@ -43,7 +43,10 @@
<view v-for="(item, index) in taskList" :key="item.id || index" class="task-card">
<!-- 卡片头部 -->
<view class="card-header">
<view class="flow-name">{{ item.flowName || '-' }}</view>
<view class="flow-name">
<text class="title-index">{{ index + 1 }}.</text>
{{ item.flowName || '-' }}
</view>
<view class="flow-status">
<u-tag
:text="getFlowStatusText(item.flowStatus)"
@@ -458,6 +461,12 @@ export default {
border-bottom: 1px solid #f0f0f0;
}
.title-index {
color: #000000;
font-weight: bold;
margin-right: 8rpx;
}
.flow-name {
font-size: 32rpx;
font-weight: bold;
+10 -1
View File
@@ -73,7 +73,10 @@
>
<!-- 卡片头部 -->
<view class="card-header">
<view class="task-title">{{ item.flowName || '-' }}</view>
<view class="task-title">
<text class="title-index">{{ index + 1 }}.</text>
{{ item.flowName || '-' }}
</view>
<view class="task-status">
<u-tag
:text="getFlowStatusText(item.flowStatus)"
@@ -842,6 +845,12 @@ export default {
border-bottom: 1px solid #f0f0f0;
}
.title-index {
color: #000000;
font-weight: bold;
margin-right: 8rpx;
}
.task-title {
font-size: 32rpx;
font-weight: bold;
+156 -80
View File
@@ -53,92 +53,110 @@
<u-empty text="暂无待办任务" mode="data"></u-empty>
</view>
<!-- 任务卡片列表 -->
<view v-else class="task-list">
<view v-for="(item, index) in taskList" :key="item.id || index" class="task-card">
<!-- 卡片头部 -->
<view class="card-header">
<view class="task-title">{{ item.flowName || '-' }}</view>
<view class="task-status">
<u-tag
:text="getFlowStatusText(item.flowStatus)"
:type="getFlowStatusType(item.flowStatus)"
size="mini"
></u-tag>
</view>
</view>
<!-- 卡片内容 -->
<view class="card-content">
<view class="info-item">
<text class="label">流程名称:</text>
<text class="value">{{ item.flowName || '-' }}</text>
</view>
<view class="info-item">
<text class="label">流程分类:</text>
<text class="value">{{ item.categoryName || '-' }}</text>
</view>
<view class="info-item">
<text class="label">任务名称:</text>
<text class="value">{{ item.nodeName || '-' }}</text>
</view>
<view class="info-item">
<text class="label">申请人:</text>
<text class="value">{{ item.createByName || '-' }}</text>
</view>
<view class="info-item">
<text class="label">办理人:</text>
<view class="assignee-tags">
<template v-if="item.assigneeNames">
<u-tag
v-for="(name, idx) in item.assigneeNames.split(',')"
:key="idx"
:text="name"
type="success"
size="mini"
style="margin-right: 8rpx; margin-bottom: 8rpx"
></u-tag>
</template>
<u-tag v-else text="无" type="info" size="mini"></u-tag>
<!-- 任务卡片列表 - 滚动加载 -->
<scroll-view
v-else
scroll-y
class="task-scroll-list"
@scrolltolower="loadMore"
:scroll-top="scrollTop"
refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
>
<view class="task-list">
<view
v-for="(item, index) in taskList"
:key="item.id || index"
class="task-card"
>
<!-- 卡片头部 -->
<view class="card-header">
<view class="task-title">
<text class="title-index">{{ index + 1 }}.</text>
{{ item.flowName || '-' }}
</view>
<view class="task-status">
<u-tag
:text="getFlowStatusText(item.flowStatus)"
:type="getFlowStatusType(item.flowStatus)"
size="mini"
></u-tag>
</view>
</view>
<view class="info-item">
<text class="label">创建时间:</text>
<text class="value">{{ formatTime(item.createTime) }}</text>
<!-- 卡片内容 -->
<view class="card-content">
<view class="info-item">
<text class="label">流程名称:</text>
<text class="value">{{ item.flowName || '-' }}</text>
</view>
<view class="info-item">
<text class="label">流程分类:</text>
<text class="value">{{ item.categoryName || '-' }}</text>
</view>
<view class="info-item">
<text class="label">任务名称:</text>
<text class="value">{{ item.nodeName || '-' }}</text>
</view>
<view class="info-item">
<text class="label">申请人:</text>
<text class="value">{{ item.createByName || '-' }}</text>
</view>
<view class="info-item">
<text class="label">办理人:</text>
<view class="assignee-tags">
<template v-if="item.assigneeNames">
<u-tag
v-for="(name, idx) in item.assigneeNames.split(',')"
:key="idx"
:text="name"
type="success"
size="mini"
style="margin-right: 8rpx; margin-bottom: 8rpx"
></u-tag>
</template>
<u-tag v-else text="无" type="info" size="mini"></u-tag>
</view>
</view>
<view class="info-item">
<text class="label">创建时间:</text>
<text class="value">{{ formatTime(item.createTime) }}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="card-actions">
<u-button
@click="handleApproval(item)"
type="primary"
size="small"
shape="circle"
>
<u-icon
name="edit-pen"
size="16"
color="#fff"
style="margin-right: 8rpx"
></u-icon>
办理
</u-button>
</view>
</view>
<!-- 操作按钮 -->
<view class="card-actions">
<u-button
@click="handleApproval(item)"
type="primary"
size="small"
shape="circle"
>
<u-icon
name="edit-pen"
size="16"
color="#fff"
style="margin-right: 8rpx"
></u-icon>
办理
</u-button>
</view>
</view>
</view>
<!-- 滚动加载更多 -->
<u-loadmore
v-if="taskList.length > 0"
:status="loadmoreStatus"
:load-text="{
loadmore: '轻轻上拉',
loading: '努力加载中',
nomore: '没有更多了',
}"
@loadmore="loadMore"
></u-loadmore>
<!-- 滚动加载更多状态 -->
<view v-if="taskList.length > 0" class="loadmore-status">
<u-loadmore
:status="loadmoreStatus"
:load-text="{
loadmore: '轻轻上拉加载更多',
loading: '努力加载中...',
nomore: '没有更多数据了',
}"
></u-loadmore>
</view>
</scroll-view>
</view>
<!-- 申请人选择弹窗 -->
@@ -229,6 +247,10 @@ export default {
total: 0,
loadmoreStatus: 'loadmore', // loadmore, loading, nomore
// 滚动相关
scrollTop: 0,
refreshing: false,
// 查询参数
queryParams: {
pageNum: 1,
@@ -262,6 +284,8 @@ export default {
async created() {
await this.loadFlowStatusDict();
// 重置滚动位置
this.scrollTop = 0;
await this.getList();
},
@@ -320,6 +344,8 @@ export default {
if (!isLoadMore) {
this.loading = true;
this.queryParams.pageNum = 1;
// 非加载更多时重置滚动位置
this.scrollTop = 0;
} else {
this.loadmoreStatus = 'loading';
}
@@ -400,6 +426,8 @@ export default {
// 搜索操作
handleQuery() {
// 重置滚动位置
this.scrollTop = 0;
this.getList();
},
@@ -413,6 +441,35 @@ export default {
this.getList(true);
},
// 下拉刷新
async onRefresh() {
this.refreshing = true;
try {
// 重置分页参数
this.queryParams.pageNum = 1;
this.loadmoreStatus = 'loadmore';
// 重新加载数据
await this.getList();
// 显示刷新成功提示
uni.showToast({
title: '刷新成功',
icon: 'success',
duration: 1500,
});
} catch (error) {
console.error('下拉刷新失败:', error);
uni.showToast({
title: '刷新失败',
icon: 'none',
duration: 1500,
});
} finally {
this.refreshing = false;
}
},
// 格式化时间
formatTime(timeStr) {
if (!timeStr) return '-';
@@ -536,6 +593,8 @@ export default {
}
this.userSelectDialog.visible = false;
// 重置滚动位置并查询
this.scrollTop = 0;
this.handleQuery();
},
@@ -717,6 +776,12 @@ export default {
.content-wrapper {
padding: 20rpx;
height: calc(100vh - 200rpx); /* 减去导航栏和搜索区域的高度 */
}
.task-scroll-list {
height: 100%;
padding: 0 20rpx;
}
.empty-container {
@@ -744,6 +809,12 @@ export default {
border-bottom: 1px solid #f0f0f0;
}
.title-index {
color: #000000;
font-weight: bold;
margin-right: 8rpx;
}
.task-title {
font-size: 32rpx;
font-weight: bold;
@@ -923,4 +994,9 @@ export default {
::v-deep .u-loadmore {
padding: 40rpx 0;
}
.loadmore-status {
padding: 20rpx 0;
text-align: center;
}
</style>
+119 -2
View File
@@ -92,6 +92,12 @@
<!-- 请假记录卡片列表 -->
<view v-else class="leave-list">
<view v-for="(item, index) in leaveList" :key="item.id || index" class="leave-card">
<!-- 序号标识 -->
<view class="card-index">
<text class="index-text">
{{ getItemIndex(index) }}
</text>
</view>
<!-- 卡片头部 -->
<view class="card-header">
<view class="leave-info">
@@ -478,7 +484,11 @@ export default {
本次加载: newData.length,
总数: this.total,
列表长度: this.leaveList.length,
分页状态: this.loadStatus,
});
// 验证序号连续性
this.validateIndexContinuity();
} else {
console.warn('获取请假记录失败:', res?.msg);
uni.showToast({
@@ -518,7 +528,12 @@ export default {
return;
}
console.log('📄 开始加载更多数据...');
console.log('📄 开始加载更多数据...', {
当前页码: this.queryParams.pageNum,
当前列表长度: this.leaveList.length,
总数: this.total,
});
this.queryParams.pageNum += 1;
await this.loadLeaveList(false);
},
@@ -604,11 +619,17 @@ export default {
// 搜索操作
handleQuery() {
console.log('🔍 执行搜索:', this.queryParams);
// 搜索时重置分页状态
this.queryParams.pageNum = 1;
this.loadStatus = 'loadmore';
this.loadLeaveList();
},
// 重置搜索
resetQuery() {
console.log('🔄 重置搜索条件...');
this.queryParams.startTime = '';
this.queryParams.endTime = '';
this.queryParams.approvalStatus = '';
@@ -616,6 +637,12 @@ export default {
this.endTimeDisplay = '';
this.startTimeValue = '';
this.endTimeValue = '';
// 重置分页状态
this.queryParams.pageNum = 1;
this.loadStatus = 'loadmore';
console.log('🔄 重置完成,开始加载数据...');
this.loadLeaveList();
},
@@ -703,6 +730,60 @@ export default {
}
},
// 计算列表项序号
getItemIndex(index) {
// 序号应该基于数据在列表中的实际位置,而不是基于当前请求的页码
// 直接使用 index + 1,因为数据已经按顺序合并到 leaveList 中
const finalIndex = index + 1;
// 调试信息
console.log('🔢 序号计算:', {
当前项索引: index,
最终序号: finalIndex,
列表总长度: this.leaveList.length,
});
return finalIndex;
},
// 验证序号连续性
validateIndexContinuity() {
if (this.leaveList.length === 0) return;
console.log('🔍 验证序号连续性...');
const expectedIndices = [];
for (let i = 0; i < this.leaveList.length; i++) {
const expectedIndex = i + 1;
const actualIndex = this.getItemIndex(i);
expectedIndices.push({ position: i, expected: expectedIndex, actual: actualIndex });
}
console.log('🔍 序号验证结果:', expectedIndices);
// 检查是否有不连续的序号
const hasDiscontinuity = expectedIndices.some((item, index) => {
if (index === 0) return false;
return item.expected !== expectedIndices[index - 1].expected + 1;
});
if (hasDiscontinuity) {
console.warn('⚠️ 发现序号不连续:', expectedIndices);
} else {
console.log('✅ 序号连续性验证通过');
}
// 额外验证:检查序号范围
const minIndex = Math.min(...expectedIndices.map((item) => item.actual));
const maxIndex = Math.max(...expectedIndices.map((item) => item.actual));
console.log('🔍 序号范围验证:', {
最小序号: minIndex,
最大序号: maxIndex,
期望范围: `1-${this.leaveList.length}`,
是否正常: minIndex === 1 && maxIndex === this.leaveList.length,
});
},
// 查看详情
handleViewDetail(item) {
const detailInfo = [
@@ -863,6 +944,26 @@ export default {
border-radius: 16rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
overflow: hidden;
position: relative;
}
// 序号标识
.card-index {
position: absolute;
top: 8rpx;
left: 8rpx;
background-color: #f5f6fa;
padding: 6rpx 12rpx;
border-radius: 8rpx;
z-index: 1;
min-width: 32rpx;
text-align: center;
}
.index-text {
font-size: 24rpx;
color: #909399;
font-weight: 500;
}
// 卡片头部
@@ -871,6 +972,7 @@ export default {
justify-content: space-between;
align-items: flex-start;
padding: 24rpx;
padding-left: 80rpx; // 为序号预留左边空间
border-bottom: 1px solid #f0f0f0;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
}
@@ -928,6 +1030,7 @@ export default {
// 卡片内容
.card-content {
padding: 24rpx;
padding-left: 80rpx; // 与头部对齐
}
.info-section {
@@ -1021,6 +1124,7 @@ export default {
display: flex;
justify-content: flex-end;
padding: 20rpx;
padding-left: 80rpx; // 与头部对齐
border-top: 1px solid #f0f0f0;
gap: 16rpx;
}
@@ -1080,8 +1184,21 @@ export default {
}
.card-header,
.card-content {
.card-content,
.card-actions {
padding: 20rpx;
padding-left: 70rpx; // 小屏幕下为序号预留左边空间
}
.card-index {
top: 6rpx;
left: 6rpx;
padding: 4rpx 8rpx;
min-width: 28rpx;
}
.index-text {
font-size: 22rpx;
}
.fab-button {
+17 -2
View File
@@ -39,7 +39,10 @@
>
<!-- 卡片头部 -->
<view class="card-header">
<view class="batch-title">{{ item.batchName || '-' }}</view>
<view class="batch-title">
<text class="batch-number">{{ index + 1 }}.</text>
<text>{{ item.batchName || '-' }}</text>
</view>
<view class="batch-status">
<u-tag
:text="item.status === '0' ? '正常' : '停用'"
@@ -90,7 +93,12 @@
></u-icon>
信息完善
</u-button>
<u-button type="info" shape="round" size="small" @click="handleViewHistory(item)">
<u-button
type="info"
shape="round"
size="small"
@click="handleViewHistory(item)"
>
<u-icon
name="clock"
size="14"
@@ -450,6 +458,13 @@ export default {
font-weight: bold;
color: #303133;
flex: 1;
display: flex;
align-items: center;
}
.batch-number {
color: #000000;
margin-right: 8rpx;
}
.batch-status {
+147 -27
View File
@@ -50,12 +50,15 @@
</view>
<!-- 加载状态 -->
<u-loading-page :loading="loading" loading-text="加载中..."></u-loading-page>
<u-loading-page
:loading="loading && queryParams.pageNum === 1"
loading-text="加载中..."
></u-loading-page>
<!-- 列表内容 -->
<view v-if="!loading" class="content-wrapper">
<view v-if="!loading || queryParams.pageNum > 1" class="content-wrapper">
<!-- 空状态 -->
<view v-if="dormViolationList.length === 0" class="empty-container">
<view v-if="dormViolationList.length === 0 && !loading" class="empty-container">
<u-empty text="暂无违纪记录" mode="data"></u-empty>
</view>
@@ -66,6 +69,13 @@
:key="item.id || index"
class="violation-card"
>
<!-- 序号标识 -->
<view class="card-index">
<text class="index-text">
{{ index + 1 }}
</text>
</view>
<!-- 卡片头部 -->
<view class="card-header">
<view class="student-info">
@@ -178,15 +188,18 @@
</view>
</view>
<!-- 加载更多 -->
<view v-if="hasMore" class="load-more">
<u-loadmore
:status="loadMoreStatus"
@loadmore="loadMore"
:loading-text="loadingText"
:loadmore-text="loadMoreText"
:nomore-text="noMoreText"
></u-loadmore>
<!-- 底部加载状态提示 -->
<view v-if="dormViolationList.length > 0" class="load-status">
<view v-if="loading && queryParams.pageNum > 1" class="loading-tip">
<u-loading-icon mode="circle" size="32"></u-loading-icon>
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="!hasMore" class="no-more-tip">
<text class="no-more-text">没有更多数据了</text>
</view>
<view v-else-if="dormViolationList.length >= 10" class="pull-tip">
<text class="pull-text">上拉加载更多</text>
</view>
</view>
</view>
</view>
@@ -205,10 +218,6 @@ export default {
dormViolationList: [],
total: 0,
hasMore: true,
loadMoreStatus: 'loadmore',
loadingText: '努力加载中...',
loadMoreText: '点击或上拉加载更多',
noMoreText: '没有更多了',
// 查询参数
queryParams: {
@@ -253,6 +262,16 @@ export default {
await this.initData();
},
// 监听页面触底事件
onReachBottom() {
this.loadMore();
},
// 监听下拉刷新事件
onPullDownRefresh() {
this.refreshData();
},
methods: {
// 返回上一页
goBack() {
@@ -296,10 +315,19 @@ export default {
// 获取违纪记录列表
async getList(isLoadMore = false) {
if (this.loading) return;
// 防止重复加载
if (this.loading) {
console.log('正在加载中,跳过本次请求');
return;
}
// 如果没有更多数据且是加载更多操作,直接返回
if (isLoadMore && !this.hasMore) {
console.log('没有更多数据,跳过加载更多');
return;
}
this.loading = true;
this.loadMoreStatus = 'loading';
try {
// 获取当前登录用户信息
@@ -326,24 +354,52 @@ export default {
if (res && res.code === 200) {
const { rows, total } = res;
// 确保 rows 是数组
const newData = Array.isArray(rows) ? rows : [];
console.log('API返回数据:', {
isLoadMore: isLoadMore,
pageNum: this.queryParams.pageNum,
apiReturnedCount: newData.length,
currentListLength: this.dormViolationList.length,
total: total,
});
if (isLoadMore) {
this.dormViolationList = [...this.dormViolationList, ...rows];
// 加载更多时,追加数据(去重处理)
const existingIds = new Set(this.dormViolationList.map((item) => item.id));
const uniqueNewData = newData.filter((item) => !existingIds.has(item.id));
console.log('加载更多去重处理:', {
existingCount: this.dormViolationList.length,
newDataCount: newData.length,
uniqueNewDataCount: uniqueNewData.length,
duplicateCount: newData.length - uniqueNewData.length,
});
this.dormViolationList = [...this.dormViolationList, ...uniqueNewData];
} else {
this.dormViolationList = rows;
// 首次加载或搜索时,替换数据
this.dormViolationList = newData;
}
this.total = total;
// 更新是否还有更多数据的状态
this.hasMore = this.dormViolationList.length < total;
console.log('获取违纪记录成功:', {
current: this.dormViolationList.length,
finalListLength: this.dormViolationList.length,
total: total,
hasMore: this.hasMore,
isLoadMore: isLoadMore,
pageNum: this.queryParams.pageNum,
});
} else {
if (!isLoadMore) {
this.dormViolationList = [];
this.total = 0;
this.hasMore = false;
}
uni.showToast({
title: res?.msg || '获取数据失败',
@@ -355,6 +411,7 @@ export default {
if (!isLoadMore) {
this.dormViolationList = [];
this.total = 0;
this.hasMore = false;
}
uni.showToast({
title: '获取数据失败',
@@ -362,13 +419,17 @@ export default {
});
} finally {
this.loading = false;
this.loadMoreStatus = this.hasMore ? 'loadmore' : 'nomore';
// 结束下拉刷新
if (!isLoadMore) {
uni.stopPullDownRefresh();
}
}
},
// 搜索操作
handleQuery() {
this.queryParams.pageNum = 1;
this.hasMore = true;
this.getList();
},
@@ -379,10 +440,18 @@ export default {
pageSize: 10,
violationTime: '',
};
this.hasMore = true;
this.getList();
},
// 加载更多
// 下拉刷新
refreshData() {
this.queryParams.pageNum = 1;
this.hasMore = true;
this.getList();
},
// 加载更多(滚动到底部触发)
loadMore() {
if (this.hasMore && !this.loading) {
this.queryParams.pageNum++;
@@ -554,13 +623,34 @@ export default {
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
overflow: hidden;
position: relative;
}
.card-index {
position: absolute;
top: 12rpx;
left: 12rpx;
width: 40rpx;
height: 40rpx;
background-color: rgba(162, 161, 161, 0.8);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
.index-text {
font-size: 22rpx;
font-weight: bold;
color: #fff;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
padding: 24rpx 24rpx 24rpx 70rpx;
border-bottom: 1px solid #f0f0f0;
background-color: #f8f9fa;
}
@@ -673,10 +763,42 @@ export default {
border-top: 1px solid #f0f0f0;
}
.load-more {
// 底部加载状态样式
.load-status {
padding: 30rpx 0;
text-align: center;
}
.loading-tip {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
}
.loading-text {
font-size: 26rpx;
color: #909399;
}
.no-more-tip {
padding: 20rpx 0;
}
.no-more-text {
font-size: 26rpx;
color: #c0c4cc;
}
.pull-tip {
padding: 20rpx 0;
}
.pull-text {
font-size: 26rpx;
color: #909399;
}
// uview组件样式调整
::v-deep .u-tag {
margin: 0;
@@ -684,13 +806,11 @@ export default {
::v-deep .u-button {
flex: 1;
// max-width: 200rpx;
border-radius: 8rpx;
}
// 搜索按钮样式
::v-deep .search-btn {
// background: linear-gradient(135deg, #007aff, #0051d9) !important;
border: none !important;
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.3) !important;
transition: all 0.3s ease !important;
+45 -32
View File
@@ -48,7 +48,10 @@
<view class="card-header">
<!-- 第一行:标题和卫生等级 -->
<view class="header-row">
<view class="check-title">{{ item.checkTitle || '-' }}</view>
<view class="check-title">
<text class="title-index">{{ index + 1 }}.</text>
{{ item.checkTitle || '-' }}
</view>
<view class="grade-section">
<u-tag
:text="$getDictLabel(xg_dorm_hygiene_grade, item.hygieneGrade)"
@@ -151,22 +154,37 @@
<!-- 操作按钮 -->
<view class="card-actions">
<!-- 查看按钮 -->
<view class="action-btn primary-btn" @click="handleView(item)">
<u-icon name="eye" size="16" color="#19be6b"></u-icon>
<text class="btn-text">查看</text>
</view>
<u-button
type="info"
size="small"
class="action-btn"
@click="handleView(item)"
>
<u-icon name="eye" size="16" color="#323233"></u-icon>
<text>查看</text>
</u-button>
<!-- 修改按钮 -->
<view class="action-btn info-btn" @click="handleUpdate(item)">
<u-icon name="edit-pen" size="16" color="#3c9cff"></u-icon>
<u-button
type="primary"
size="small"
class="action-btn"
@click="handleUpdate(item)"
>
<u-icon name="edit-pen" size="16" color="#ffffff"></u-icon>
<text class="btn-text">修改</text>
</view>
</u-button>
<!-- 删除按钮 -->
<view class="action-btn danger-btn" @click="handleDelete(item)">
<u-icon name="trash" size="16" color="#f56c6c"></u-icon>
<u-button
type="error"
size="small"
class="action-btn"
@click="handleDelete(item)"
>
<u-icon name="trash" size="16" color="#ffffff"></u-icon>
<text class="btn-text">删除</text>
</view>
</u-button>
</view>
</view>
</view>
@@ -608,6 +626,12 @@ export default {
flex: 1;
}
.title-index {
color: #000000;
font-weight: bold;
margin-right: 8rpx;
}
.check-level {
flex-shrink: 0;
}
@@ -733,37 +757,26 @@ export default {
.action-btn {
display: flex;
flex-direction: column;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 16rpx 20rpx;
border-radius: 12rpx;
min-width: 120rpx;
transition: all 0.3s ease;
background-color: transparent;
border-radius: 20rpx;
&:active {
transform: scale(0.95);
opacity: 0.8;
::v-deep .u-button {
border-radius: 20rpx;
padding: 16rpx 20rpx;
min-width: 120rpx;
height: auto;
line-height: 1.2;
}
}
.btn-text {
font-size: 24rpx;
margin-top: 8rpx;
margin-left: 8rpx;
font-weight: 500;
}
.primary-btn .btn-text {
color: #19be6b;
}
.info-btn .btn-text {
color: #3c9cff;
}
.danger-btn .btn-text {
color: #f56c6c;
color: #ffffff;
}
// 分页容器
+74 -65
View File
@@ -60,7 +60,10 @@
<view class="card-header">
<!-- 第一行:标题和卫生等级 -->
<view class="header-row">
<view class="check-title">{{ item.checkTitle || '-' }}</view>
<view class="check-title">
<text class="check-index">{{ index + 1 }}.</text>
<text>{{ item.checkTitle || '-' }}</text>
</view>
<view class="grade-section">
<u-tag
:text="$getDictLabel(xg_dorm_hygiene_grade, item.hygieneGrade)"
@@ -189,8 +192,8 @@
</view>
</view>
<!-- 分页加载(已废弃,因为接口返回完整列表) -->
<!-- <view v-if="checkList.length > 0" class="pagination-container">
<!-- 分页加载 -->
<view v-if="checkList.length > 0" class="pagination-container">
<u-loadmore
:status="loadStatus"
@loadmore="loadMore"
@@ -198,7 +201,7 @@
loading-text="正在加载..."
no-more-text="没有更多数据了"
/>
</view> -->
</view>
<!-- 悬浮添加按钮 -->
<!-- <view class="fab-button" @click="handleAdd">
@@ -227,7 +230,6 @@ export default {
// 检查记录列表
checkList: [],
originalCheckList: [], // 原始数据,用于前端过滤
total: 0,
// 分页加载状态
@@ -260,11 +262,11 @@ export default {
uni.$off('refreshCheckList', this.handleRefreshEvent);
},
// 页面滚动到底部事件(已废弃,因为接口返回完整列表)
// onReachBottom() {
// console.log('🔄 触发滚动到底部事件,开始加载更多...');
// this.loadMore();
// },
// 页面滚动到底部事件
onReachBottom() {
console.log('🔄 触发滚动到底部事件,开始加载更多...');
this.loadMore();
},
methods: {
// 处理刷新事件
@@ -313,7 +315,11 @@ export default {
this.loading = true;
this.queryParams.pageNum = 1;
this.checkList = [];
this.loadStatus = 'loadmore';
} else {
if (this.loadStatus === 'loading' || this.loadStatus === 'nomore') {
return;
}
this.loadStatus = 'loading';
}
@@ -331,35 +337,50 @@ export default {
console.log('📋 开始加载当前学生的宿舍卫生检查记录...', {
学生ID: currentUser.userId,
查询参数: this.queryParams,
是否刷新: isRefresh,
});
// 调用根据学生ID查询卫生检查记录的接口
// 构建请求参数
const params = {
pageNum: this.queryParams.pageNum,
pageSize: this.queryParams.pageSize,
};
// 添加搜索条件
if (this.queryParams.checkTitle && this.queryParams.checkTitle.trim()) {
params.checkTitle = this.queryParams.checkTitle.trim();
}
// 调用分页接口
const res = await request({
url: `/dormHygieneCheck/dormHygieneCheck/student/${currentUser.userId}`,
url: `/dormHygieneCheck/dormHygieneCheck/student/${currentUser.userId}/page`,
method: 'GET',
params: params,
});
if (res && res.code === 200) {
const newData = res.data || [];
const newData = res.rows || [];
const total = res.total || 0;
if (isRefresh) {
this.originalCheckList = newData; // 保存原始数据
this.checkList = newData;
} else {
this.originalCheckList = [...this.originalCheckList, ...newData];
this.checkList = [...this.checkList, ...newData];
}
this.total = newData.length;
this.total = total;
// 由于getHygieneChecksByStudentId返回的是完整列表,不需要分页加载
this.loadStatus = 'nomore';
// 判断是否还有更多数据
const hasMore = this.queryParams.pageNum * this.queryParams.pageSize < total;
this.loadStatus = hasMore ? 'loadmore' : 'nomore';
console.log('✅ 学生宿舍卫生检查记录加载成功:', {
学生ID: currentUser.userId,
当前页码: this.queryParams.pageNum,
本次加载: newData.length,
总数: this.total,
总数: total,
列表长度: this.checkList.length,
是否还有更多: hasMore,
});
} else {
console.warn('获取学生宿舍卫生检查记录失败:', res?.msg);
@@ -368,9 +389,7 @@ export default {
icon: 'none',
});
if (!isRefresh) {
this.loadStatus = 'loadmore';
}
this.loadStatus = isRefresh ? 'loadmore' : 'loadmore';
}
} catch (error) {
console.error('❌ 加载学生宿舍卫生检查记录失败:', error);
@@ -379,9 +398,7 @@ export default {
icon: 'none',
});
if (!isRefresh) {
this.loadStatus = 'loadmore';
}
this.loadStatus = isRefresh ? 'loadmore' : 'loadmore';
} finally {
if (isRefresh) {
this.loading = false;
@@ -389,51 +406,35 @@ export default {
}
},
// 加载更多(已废弃,因为getHygieneChecksByStudentId返回完整列表)
// 加载更多
async loadMore() {
console.log('📄 当前接口返回完整列表,无需分页加载');
// 由于getHygieneChecksByStudentId返回的是完整列表,不需要分页加载
this.loadStatus = 'nomore';
console.log('📄 触发加载更多,当前状态:', this.loadStatus);
if (this.loadStatus !== 'loadmore') {
console.log('📄 当前状态不允许加载更多:', this.loadStatus);
return;
}
// 增加页码,加载下一页数据
this.queryParams.pageNum += 1;
console.log('📄 加载第', this.queryParams.pageNum, '页数据');
await this.loadCheckList(false);
},
// 搜索操作(前端过滤)
// 搜索操作(后端过滤)
handleQuery() {
console.log('🔍 执行前端搜索过滤:', this.queryParams);
// 由于接口返回完整列表,在前端进行过滤
this.filterCheckList();
console.log('🔍 执行搜索查询:', this.queryParams);
// 重新从第一页开始搜索
this.loadCheckList(true);
},
// 重置搜索
resetQuery() {
this.queryParams.checkTitle = '';
this.filterCheckList();
},
// 前端过滤检查记录列表
filterCheckList() {
if (!this.originalCheckList || this.originalCheckList.length === 0) {
this.checkList = [];
return;
}
let filteredList = [...this.originalCheckList];
// 按检查标题过滤
if (this.queryParams.checkTitle && this.queryParams.checkTitle.trim()) {
const searchTerm = this.queryParams.checkTitle.trim().toLowerCase();
filteredList = filteredList.filter(
(item) => item.checkTitle && item.checkTitle.toLowerCase().includes(searchTerm),
);
}
this.checkList = filteredList;
this.total = filteredList.length;
console.log('✅ 前端过滤完成:', {
原始数据: this.originalCheckList.length,
过滤后: this.checkList.length,
搜索条件: this.queryParams.checkTitle,
});
console.log('🔄 重置搜索条件');
// 重新加载数据
this.loadCheckList(true);
},
// 获取检查级别类型
@@ -655,6 +656,14 @@ export default {
font-weight: bold;
color: #303133;
flex: 1;
display: flex;
align-items: baseline;
}
.check-index {
color: #000000;
font-weight: bold;
margin-right: 8rpx;
}
.check-level {
@@ -815,10 +824,10 @@ export default {
color: #f56c6c;
}
// 分页容器(已废弃)
// .pagination-container {
// padding: 20rpx;
// }
// 分页容器
.pagination-container {
padding: 20rpx;
}
// 搜索按钮样式
::v-deep .search-btn {
+206 -119
View File
@@ -17,101 +17,126 @@
<u-empty text="暂无可申请的考核计划" mode="data"></u-empty>
</view>
<!-- 考核计划列表 -->
<view v-else class="schedule-list">
<view
v-for="(item, index) in assessmentScheduleList"
:key="item.id || index"
class="schedule-card"
>
<!-- 卡片头部 -->
<view class="card-header">
<view class="schedule-info">
<text class="schedule-name">{{ item.scheduleName || '暂无' }}</text>
<view class="schedule-meta">
<text class="academic-info">
{{ item.academicYear || '暂无' }}
{{ getSemesterText(item.semester) }}
<!-- 考核计划列表 - 滚动加载 -->
<scroll-view
v-else
class="schedule-scroll-view"
scroll-y
@scrolltolower="handleScrollToLower"
:lower-threshold="100"
>
<view class="schedule-list">
<view
v-for="(item, index) in assessmentScheduleList"
:key="item.id || index"
class="schedule-card"
>
<!-- 卡片头部 -->
<view class="card-header">
<view class="schedule-info">
<view class="schedule-name-wrapper">
<text class="schedule-index">{{ index + 1 }}.</text>
<text class="schedule-name">
{{ item.scheduleName || '暂无' }}
</text>
</view>
<view class="schedule-meta">
<text class="academic-info">
{{ item.academicYear || '暂无' }}
{{ getSemesterText(item.semester) }}
</text>
</view>
</view>
<view class="status-info">
<u-tag
:text="getStatusText(item.status)"
:type="getStatusTagType(item.status)"
size="mini"
></u-tag>
</view>
</view>
<!-- 卡片内容 -->
<view class="card-content">
<view class="schedule-description">
<text class="description-text">
{{ item.description || '暂无说明' }}
</text>
</view>
<view class="time-info">
<view class="time-item">
<text class="time-label">开始时间:</text>
<text class="time-value">{{ formatTime(item.startTime) }}</text>
</view>
<view class="time-item">
<text class="time-label">结束时间:</text>
<text class="time-value">{{ formatTime(item.endTime) }}</text>
</view>
<view class="time-item">
<text class="time-label">数据统计开始时间:</text>
<text class="time-value">
{{ formatTime(item.dataStartTime) }}
</text>
</view>
<view class="time-item">
<text class="time-label">数据统计结束时间:</text>
<text class="time-value">
{{ formatTime(item.dataEndTime) }}
</text>
</view>
</view>
<view class="plan-info">
<text class="plan-name">
考核方案: {{ item.planName || '暂无' }}
</text>
</view>
</view>
<view class="status-info">
<u-tag
:text="getStatusText(item.status)"
:type="getStatusTagType(item.status)"
size="mini"
></u-tag>
<!-- 卡片操作按钮 -->
<view class="card-actions">
<u-button
type="info"
size="small"
icon="eye"
shape="round"
@click="handleViewSchedule(item)"
>
查看计划
</u-button>
<u-button
v-if="canSubmitApplication(item)"
type="primary"
size="small"
icon="edit-pen"
shape="round"
@click="handleSubmitApplication(item)"
>
提交申请
</u-button>
<u-button
type="warning"
size="small"
icon="file-text"
shape="round"
@click="handleViewApplication(item)"
>
查看申请
</u-button>
</view>
</view>
<!-- 卡片内容 -->
<view class="card-content">
<view class="schedule-description">
<text class="description-text">
{{ item.description || '暂无说明' }}
</text>
<!-- 加载更多状态提示 -->
<view v-if="assessmentScheduleList.length > 0" class="load-more">
<view v-if="loadingMore" class="loading-more">
<u-loading-icon mode="circle" size="16"></u-loading-icon>
<text class="loading-text">正在加载更多...</text>
</view>
<view class="time-info">
<view class="time-item">
<text class="time-label">开始时间:</text>
<text class="time-value">{{ formatTime(item.startTime) }}</text>
</view>
<view class="time-item">
<text class="time-label">结束时间:</text>
<text class="time-value">{{ formatTime(item.endTime) }}</text>
</view>
<view class="time-item">
<text class="time-label">数据统计开始时间:</text>
<text class="time-value">{{ formatTime(item.dataStartTime) }}</text>
</view>
<view class="time-item">
<text class="time-label">数据统计结束时间:</text>
<text class="time-value">{{ formatTime(item.dataEndTime) }}</text>
</view>
<view v-else class="load-complete-text">
{{ getNoMoreText() }}
</view>
<view class="plan-info">
<text class="plan-name">考核方案: {{ item.planName || '暂无' }}</text>
</view>
</view>
<!-- 卡片操作按钮 -->
<view class="card-actions">
<u-button
type="info"
size="small"
icon="eye"
shape="round"
@click="handleViewSchedule(item)"
>
查看计划
</u-button>
<u-button
v-if="canSubmitApplication(item)"
type="primary"
size="small"
icon="edit-pen"
shape="round"
@click="handleSubmitApplication(item)"
>
提交申请
</u-button>
<u-button
type="warning"
size="small"
icon="file-text"
shape="round"
@click="handleViewApplication(item)"
>
查看申请
</u-button>
</view>
</view>
</view>
<!-- 分页加载 -->
<view v-if="hasMore && assessmentScheduleList.length > 0" class="load-more">
<u-loadmore :status="loadStatus" @loadmore="loadMore"></u-loadmore>
</view>
</scroll-view>
</view>
<!-- 查看计划详情弹窗 -->
@@ -231,6 +256,7 @@ export default {
total: 0,
hasMore: true,
loadStatus: 'loadmore',
loadingMore: false,
// 查询参数
queryParams: {
@@ -276,9 +302,14 @@ export default {
},
// 获取考核计划列表
async getList(loadMore = false) {
if (!loadMore) {
async getList(isLoadMore = false) {
if (isLoadMore) {
this.loadingMore = true;
} else {
this.loading = true;
// 重置分页参数
this.queryParams.pageNum = 1;
this.assessmentScheduleList = [];
}
try {
@@ -290,38 +321,49 @@ export default {
if (res && res.code === 200) {
const rows = res.rows || [];
const total = res.total || 0;
if (loadMore) {
this.assessmentScheduleList = this.assessmentScheduleList.concat(rows);
if (isLoadMore) {
// 加载更多时追加数据
this.assessmentScheduleList = [...this.assessmentScheduleList, ...rows];
} else {
// 首次加载时直接设置数据
this.assessmentScheduleList = rows;
}
this.total = res.total || 0;
this.total = total;
if (rows.length < this.queryParams.pageSize) {
this.hasMore = false;
this.loadStatus = 'nomore';
} else {
this.hasMore = true;
this.loadStatus = 'loadmore';
}
// 判断是否还有更多数据
const currentTotal = this.assessmentScheduleList.length;
this.hasMore = currentTotal < total;
this.loadStatus = this.hasMore ? 'loadmore' : 'nomore';
console.log('获取考核计划数据成功:', {
当前页: this.queryParams.pageNum,
返回条数: rows.length,
累计条数: currentTotal,
总数量: total,
是否还有更多: this.hasMore,
});
} else {
this.handleRequestError(res?.msg || '获取数据失败', loadMore);
this.handleRequestError(res?.msg || '获取数据失败');
}
} catch (error) {
console.error('获取数据失败:', error);
this.handleRequestError('获取数据失败', loadMore);
this.handleRequestError('获取数据失败');
} finally {
if (!loadMore) {
if (isLoadMore) {
this.loadingMore = false;
} else {
this.loading = false;
}
}
},
// 错误处理
handleRequestError(message, isLoadMore = false) {
if (!isLoadMore) {
handleRequestError(message) {
if (this.queryParams.pageNum === 1) {
// 首次加载失败,清空数据
this.assessmentScheduleList = [];
this.total = 0;
}
@@ -334,22 +376,22 @@ export default {
});
},
// 加载更多
async loadMore() {
if (!this.hasMore || this.loadStatus !== 'loadmore') {
// 滚动到底部触发加载更多
async handleScrollToLower() {
if (!this.hasMore || this.loadingMore || this.loading) {
console.log('无需加载更多数据:', {
hasMore: this.hasMore,
loadingMore: this.loadingMore,
loading: this.loading,
});
return;
}
this.loadStatus = 'loading';
this.queryParams.pageNum += 1;
console.log('滚动到底部,开始加载更多数据');
try {
await this.getList(true);
} catch (error) {
console.error('加载更多数据失败:', error);
this.queryParams.pageNum -= 1;
this.loadStatus = 'loadmore';
}
// 页码加1,加载下一页
this.queryParams.pageNum += 1;
await this.getList(true);
},
// 查看考核计划信息
@@ -466,9 +508,6 @@ export default {
// 提交成功回调
handleSuccess() {
this.queryParams.pageNum = 1;
this.hasMore = true;
this.loadStatus = 'loadmore';
this.getList();
},
@@ -514,6 +553,16 @@ export default {
};
return getDictTagType(this.assessmentScheduleStatus, status, fallbackMap);
},
// 获取没有更多数据文本
getNoMoreText() {
if (this.hasMore) {
return `已加载${this.assessmentScheduleList.length}条,共${this.total}条`;
} else if (this.total > 0) {
return `已全部加载完毕,共${this.total}条考核计划`;
}
return '暂无数据';
},
},
};
</script>
@@ -526,6 +575,11 @@ export default {
.content-wrapper {
padding: 20rpx;
height: calc(100vh - 88rpx); /* 减去导航栏高度 */
}
.schedule-scroll-view {
height: 100%;
}
.empty-container {
@@ -558,12 +612,25 @@ export default {
flex: 1;
}
.schedule-name-wrapper {
display: flex;
align-items: center;
margin-bottom: 12rpx;
}
.schedule-index {
font-size: 32rpx;
font-weight: bold;
color: #000000;
margin-right: 8rpx;
min-width: 40rpx;
}
.schedule-name {
font-size: 32rpx;
font-weight: bold;
color: #303133;
display: block;
margin-bottom: 12rpx;
flex: 1;
}
.schedule-meta {
@@ -640,6 +707,26 @@ export default {
padding: 20rpx 0;
}
.loading-more {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 0;
gap: 12rpx;
}
.loading-text {
font-size: 24rpx;
color: #909399;
}
.load-complete-text {
text-align: center;
font-size: 24rpx;
color: #909399;
padding: 20rpx 0;
}
/* 弹窗样式 */
.schedule-detail-popup {
background-color: #fff;
+90 -14
View File
@@ -26,7 +26,10 @@
>
<!-- 卡片头部 -->
<view class="card-header">
<view class="record-title">{{ item.disciplineTypeName || '-' }}</view>
<view class="record-title">
<text class="record-number">{{ index + 1 }}.</text>
{{ item.disciplineTypeName || '-' }}
</view>
<view class="record-status">
<u-tag
:text="getDictLabel(studentDisciplineStatus, item.status)"
@@ -114,9 +117,15 @@
</view>
</view>
<!-- 滚动加载提示 -->
<view v-if="hasMore && disciplineRecordList.length > 0" class="load-more">
<u-loadmore :status="loadStatus" @loadmore="loadMore"></u-loadmore>
<!-- 底部加载状态提示 -->
<view v-if="disciplineRecordList.length > 0" class="load-status">
<view v-if="loadingMore" class="loading-tip">
<u-loading-icon></u-loading-icon>
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="!hasMore" class="no-more-tip">
<text>已加载全部数据</text>
</view>
</view>
</view>
@@ -143,10 +152,10 @@ export default {
data() {
return {
loading: false,
loadingMore: false, // 加载更多状态
disciplineRecordList: [],
total: 0,
hasMore: true,
loadStatus: 'loadmore',
// 查询参数
queryParams: {
@@ -172,6 +181,16 @@ export default {
}
},
// 滚动到底部时自动加载更多
onReachBottom() {
this.loadMore();
},
// 下拉刷新
onPullDownRefresh() {
this.handleRefresh();
},
methods: {
// 返回上一页
goBack() {
@@ -205,6 +224,8 @@ export default {
async getList(loadMore = false) {
if (!loadMore) {
this.loading = true;
} else {
this.loadingMore = true;
}
try {
@@ -230,10 +251,8 @@ export default {
// 判断是否还有更多数据
if (rows.length < this.queryParams.pageSize) {
this.hasMore = false;
this.loadStatus = 'nomore';
} else {
this.hasMore = true;
this.loadStatus = 'loadmore';
}
} else {
if (!loadMore) {
@@ -241,7 +260,6 @@ export default {
this.total = 0;
}
this.hasMore = false;
this.loadStatus = 'nomore';
uni.showToast({
title: res?.msg || '获取数据失败',
@@ -256,7 +274,6 @@ export default {
this.total = 0;
}
this.hasMore = false;
this.loadStatus = 'nomore';
uni.showToast({
title: '获取数据失败',
@@ -265,17 +282,19 @@ export default {
} finally {
if (!loadMore) {
this.loading = false;
} else {
this.loadingMore = false;
}
}
},
// 加载更多数据
async loadMore() {
if (!this.hasMore || this.loadStatus !== 'loadmore') {
// 防止重复加载
if (!this.hasMore || this.loadingMore || this.loading) {
return;
}
this.loadStatus = 'loading';
this.queryParams.pageNum += 1;
try {
@@ -283,7 +302,25 @@ export default {
} catch (error) {
console.error('加载更多数据失败:', error);
this.queryParams.pageNum -= 1; // 回退页码
this.loadStatus = 'loadmore';
uni.showToast({
title: '加载失败,请重试',
icon: 'none',
});
}
},
// 下拉刷新处理
async handleRefresh() {
this.queryParams.pageNum = 1;
this.hasMore = true;
try {
await this.getList();
} catch (error) {
console.error('刷新数据失败:', error);
} finally {
uni.stopPullDownRefresh();
}
},
@@ -318,7 +355,6 @@ export default {
// 重新加载列表
this.queryParams.pageNum = 1;
this.hasMore = true;
this.loadStatus = 'loadmore';
this.getList();
},
@@ -354,6 +390,7 @@ export default {
.content-wrapper {
padding: 20rpx;
padding-bottom: 40rpx; // 增加底部间距
}
.empty-container {
@@ -386,6 +423,15 @@ export default {
font-weight: bold;
color: #303133;
flex: 1;
display: flex;
align-items: center;
}
.record-number {
font-size: 28rpx;
font-weight: 600;
margin-right: 8rpx;
min-width: 40rpx;
}
.record-status {
@@ -434,12 +480,42 @@ export default {
gap: 16rpx;
}
.load-more {
// 底部加载状态样式
.load-status {
display: flex;
justify-content: center;
align-items: center;
padding: 40rpx 20rpx;
margin-top: 20rpx;
}
.loading-tip {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
.loading-text {
font-size: 28rpx;
color: #999;
}
}
.no-more-tip {
text {
font-size: 28rpx;
color: #999;
}
}
// uview组件样式调整
::v-deep .u-tag {
margin: 0;
}
// 加载图标样式调整
::v-deep .u-loading-icon {
width: 32rpx;
height: 32rpx;
}
</style>