修改bug,优化程序。

This commit is contained in:
胡圣锋 2025-11-30 11:03:51 +08:00
commit c014532706
31 changed files with 2199 additions and 433 deletions

View File

@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import com.ddnai.common.annotation.Log; import com.ddnai.common.annotation.Log;
import com.ddnai.common.core.controller.BaseController; import com.ddnai.common.core.controller.BaseController;
@ -36,7 +37,6 @@ import com.ddnai.system.service.psychology.IPsyAssessmentService;
import com.ddnai.system.service.psychology.IPsyAssessmentAnswerService; import com.ddnai.system.service.psychology.IPsyAssessmentAnswerService;
import com.ddnai.system.service.psychology.IPsyScaleItemService; import com.ddnai.system.service.psychology.IPsyScaleItemService;
import java.util.ArrayList; import java.util.ArrayList;
import java.math.BigDecimal;
/** /**
* 测评记录 信息操作处理 * 测评记录 信息操作处理
@ -87,10 +87,33 @@ public class PsyAssessmentController extends BaseController
* 获取暂停的测评列表 * 获取暂停的测评列表
*/ */
@GetMapping("/pausedList") @GetMapping("/pausedList")
public AjaxResult pausedList() public AjaxResult pausedList(@RequestParam(value = "userId", required = false) Long userId)
{ {
Long userId = SecurityUtils.getUserId(); Long currentUserId = SecurityUtils.getUserId();
List<PsyAssessment> list = assessmentService.selectPausedAssessmentList(userId); boolean hasManagePerm = false;
try
{
hasManagePerm = SecurityUtils.hasPermi("psychology:assessment:list");
}
catch (Exception ignored)
{
}
Long targetUserId = null;
if (userId != null)
{
if (!userId.equals(currentUserId) && !hasManagePerm)
{
return error("无权查看其他用户的暂停测评记录");
}
targetUserId = userId;
}
else if (!hasManagePerm)
{
targetUserId = currentUserId;
}
List<PsyAssessment> list = assessmentService.selectPausedAssessmentList(targetUserId);
return success(list); return success(list);
} }

View File

@ -1,7 +1,6 @@
package com.ddnai.web.controller.psychology; package com.ddnai.web.controller.psychology;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -74,6 +73,7 @@ public class PsyScaleController extends BaseController
public TableDataInfo list(PsyScale scale, @RequestParam(required = false, defaultValue = "true") Boolean includeQuestionnaire) public TableDataInfo list(PsyScale scale, @RequestParam(required = false, defaultValue = "true") Boolean includeQuestionnaire)
{ {
Set<Long> allowedScaleIds = resolveAllowedScaleIdsForCurrentUser(); Set<Long> allowedScaleIds = resolveAllowedScaleIdsForCurrentUser();
Set<Long> restrictedScaleIds = resolveRestrictedScaleIds();
boolean needPermissionFilter = allowedScaleIds != null; boolean needPermissionFilter = allowedScaleIds != null;
// 如果需要包含问卷需要先查询所有数据合并后再分页 // 如果需要包含问卷需要先查询所有数据合并后再分页
@ -91,7 +91,7 @@ public class PsyScaleController extends BaseController
} }
} }
scaleList = filterScaleListByPermission(scaleList, allowedScaleIds); scaleList = filterScaleListByPermission(scaleList, allowedScaleIds, restrictedScaleIds);
// 查询问卷列表不分页因为需要合并后再分页 // 查询问卷列表不分页因为需要合并后再分页
PsyQuestionnaire questionnaireQuery = new PsyQuestionnaire(); PsyQuestionnaire questionnaireQuery = new PsyQuestionnaire();
@ -119,7 +119,7 @@ public class PsyScaleController extends BaseController
scaleList.add(scaleItem); scaleList.add(scaleItem);
} }
scaleList = filterScaleListByPermission(scaleList, allowedScaleIds); scaleList = filterScaleListByPermission(scaleList, allowedScaleIds, restrictedScaleIds);
// 按排序顺序和创建时间排序 // 按排序顺序和创建时间排序
scaleList.sort((a, b) -> { scaleList.sort((a, b) -> {
@ -157,7 +157,7 @@ public class PsyScaleController extends BaseController
} }
} }
scaleList = filterScaleListByPermission(scaleList, allowedScaleIds); scaleList = filterScaleListByPermission(scaleList, allowedScaleIds, restrictedScaleIds);
if (needPermissionFilter) if (needPermissionFilter)
{ {
@ -193,7 +193,6 @@ public class PsyScaleController extends BaseController
scale.setRemark(questionnaire.getRemark()); scale.setRemark(questionnaire.getRemark());
return scale; return scale;
} }
/** /**
* 构建手动分页结果适用于自定义过滤后的列表 * 构建手动分页结果适用于自定义过滤后的列表
*/ */
@ -224,7 +223,7 @@ public class PsyScaleController extends BaseController
* @param scaleList 原始列表 * @param scaleList 原始列表
* @param allowedScaleIds null表示无需过滤非null表示仅可访问允许集合中的量表 * @param allowedScaleIds null表示无需过滤非null表示仅可访问允许集合中的量表
*/ */
private List<PsyScale> filterScaleListByPermission(List<PsyScale> scaleList, Set<Long> allowedScaleIds) private List<PsyScale> filterScaleListByPermission(List<PsyScale> scaleList, Set<Long> allowedScaleIds, Set<Long> restrictedScaleIds)
{ {
if (allowedScaleIds == null || scaleList == null) if (allowedScaleIds == null || scaleList == null)
{ {
@ -238,13 +237,16 @@ public class PsyScaleController extends BaseController
continue; continue;
} }
String sourceType = scale.getSourceType(); String sourceType = scale.getSourceType();
Long scaleId = scale.getScaleId();
if ("questionnaire".equalsIgnoreCase(sourceType)) if ("questionnaire".equalsIgnoreCase(sourceType))
{ {
// 问卷类型默认对所有学员开放 boolean restricted = restrictedScaleIds != null && restrictedScaleIds.contains(scaleId);
if (!restricted)
{
filtered.add(scale); filtered.add(scale);
continue; continue;
} }
Long scaleId = scale.getScaleId(); }
if (scaleId != null && allowedScaleIds.contains(scaleId)) if (scaleId != null && allowedScaleIds.contains(scaleId))
{ {
filtered.add(scale); filtered.add(scale);
@ -586,6 +588,24 @@ public class PsyScaleController extends BaseController
} }
} }
private Set<Long> resolveRestrictedScaleIds()
{
try
{
List<Long> ids = scalePermissionService.selectAllScaleIdsWithPermission();
if (ids == null || ids.isEmpty())
{
return java.util.Collections.emptySet();
}
return new java.util.HashSet<>(ids);
}
catch (Exception e)
{
logger.warn("解析量表权限限制列表失败: {}", e.getMessage());
return java.util.Collections.emptySet();
}
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Map<String, Object> normalizeScaleImportMap(Map<String, Object> root) private Map<String, Object> normalizeScaleImportMap(Map<String, Object> root)
{ {

View File

@ -24,6 +24,7 @@ import com.ddnai.common.enums.BusinessType;
import com.ddnai.common.utils.SecurityUtils; import com.ddnai.common.utils.SecurityUtils;
import com.ddnai.common.utils.StringUtils; import com.ddnai.common.utils.StringUtils;
import com.ddnai.common.utils.poi.ExcelUtil; import com.ddnai.common.utils.poi.ExcelUtil;
import com.ddnai.system.domain.dto.ImportProgress;
import com.ddnai.system.domain.psychology.PsyUserProfile; import com.ddnai.system.domain.psychology.PsyUserProfile;
import com.ddnai.system.service.ISysDeptService; import com.ddnai.system.service.ISysDeptService;
import com.ddnai.system.service.ISysPostService; import com.ddnai.system.service.ISysPostService;
@ -292,4 +293,15 @@ public class PsyUserProfileController extends BaseController
String message = profileService.importProfile(profileList, updateSupport, operName); String message = profileService.importProfile(profileList, updateSupport, operName);
return success(message); return success(message);
} }
/**
* 查询导入进度
*/
@PreAuthorize("@ss.hasPermi('psychology:profile:import')")
@GetMapping("/importProgress")
public AjaxResult getImportProgress()
{
ImportProgress progress = profileService.getImportProgress(getUsername());
return success(progress);
}
} }

View File

@ -0,0 +1,150 @@
package com.ddnai.system.domain.dto;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 通用导入进度信息
*/
public class ImportProgress implements Serializable
{
private static final long serialVersionUID = 1L;
/** 总数量 */
private int total;
/** 已处理数量 */
private int processed;
/** 成功数量 */
private int success;
/** 失败数量 */
private int failure;
/** 状态processing、success、failed */
private String status;
/** 状态描述/提示 */
private String message;
/** 开始时间 */
private LocalDateTime startTime;
/** 最近更新时间 */
private LocalDateTime lastUpdateTime;
/** 过期时间戳(毫秒) */
private long expireAt;
public int getTotal()
{
return total;
}
public void setTotal(int total)
{
this.total = total;
}
public int getProcessed()
{
return processed;
}
public void setProcessed(int processed)
{
this.processed = processed;
}
public int getSuccess()
{
return success;
}
public void setSuccess(int success)
{
this.success = success;
}
public int getFailure()
{
return failure;
}
public void setFailure(int failure)
{
this.failure = failure;
}
public String getStatus()
{
return status;
}
public void setStatus(String status)
{
this.status = status;
}
public String getMessage()
{
return message;
}
public void setMessage(String message)
{
this.message = message;
}
public LocalDateTime getStartTime()
{
return startTime;
}
public void setStartTime(LocalDateTime startTime)
{
this.startTime = startTime;
}
public LocalDateTime getLastUpdateTime()
{
return lastUpdateTime;
}
public void setLastUpdateTime(LocalDateTime lastUpdateTime)
{
this.lastUpdateTime = lastUpdateTime;
}
public long getExpireAt()
{
return expireAt;
}
public void setExpireAt(long expireAt)
{
this.expireAt = expireAt;
}
public boolean isExpired(long now)
{
return expireAt > 0 && now > expireAt;
}
public ImportProgress copy()
{
ImportProgress copy = new ImportProgress();
copy.setTotal(this.total);
copy.setProcessed(this.processed);
copy.setSuccess(this.success);
copy.setFailure(this.failure);
copy.setStatus(this.status);
copy.setMessage(this.message);
copy.setStartTime(this.startTime);
copy.setLastUpdateTime(this.lastUpdateTime);
copy.setExpireAt(this.expireAt);
return copy;
}
}

View File

@ -46,6 +46,9 @@ public class PsyScalePermission extends BaseEntity
/** 量表名称(关联查询字段,不存储在表中) */ /** 量表名称(关联查询字段,不存储在表中) */
private String scaleName; private String scaleName;
/** 来源类型scale/questionnaire */
private String sourceType;
/** 部门名称(关联查询字段,不存储在表中) */ /** 部门名称(关联查询字段,不存储在表中) */
private String deptName; private String deptName;
@ -155,6 +158,16 @@ public class PsyScalePermission extends BaseEntity
this.scaleName = scaleName; this.scaleName = scaleName;
} }
public String getSourceType()
{
return sourceType;
}
public void setSourceType(String sourceType)
{
this.sourceType = sourceType;
}
public String getDeptName() public String getDeptName()
{ {
return deptName; return deptName;
@ -202,6 +215,8 @@ public class PsyScalePermission extends BaseEntity
.append("updateBy", getUpdateBy()) .append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime()) .append("updateTime", getUpdateTime())
.append("remark", getRemark()) .append("remark", getRemark())
.append("scaleName", getScaleName())
.append("sourceType", getSourceType())
.toString(); .toString();
} }
} }

View File

@ -34,7 +34,7 @@ public class PsyUserProfile extends BaseEntity
private String profileData; private String profileData;
/** 姓名 */ /** 姓名 */
@Excel(name = "罪犯姓名", sort = 2) @Excel(name = "罪犯姓名(必填)", sort = 2)
private String userName; private String userName;
/** 电话 */ /** 电话 */
@ -42,14 +42,15 @@ public class PsyUserProfile extends BaseEntity
/** 生日 */ /** 生日 */
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "出生日期", sort = 6, width = 20, dateFormat = "yyyy-MM-dd")
private java.util.Date birthday; private java.util.Date birthday;
/** 监狱 */ /** 监狱 */
@Excel(name = "监狱", sort = 3) @Excel(name = "监狱(必填)", sort = 3)
private String prison; private String prison;
/** 监区 */ /** 监区 */
@Excel(name = "监区", sort = 4) @Excel(name = "监区(必填)", sort = 4)
private String prisonArea; private String prisonArea;
/** 性别 */ /** 性别 */
@ -57,38 +58,38 @@ public class PsyUserProfile extends BaseEntity
private String gender; private String gender;
/** 民族 */ /** 民族 */
@Excel(name = "民族", sort = 6) @Excel(name = "民族", sort = 7)
private String nation; private String nation;
/** 文化程度 */ /** 文化程度 */
@Excel(name = "文化程度", sort = 7) @Excel(name = "文化程度", sort = 8)
private String educationLevel; private String educationLevel;
/** 罪名 */ /** 罪名 */
@Excel(name = "罪名", sort = 8) @Excel(name = "罪名", sort = 9)
private String crimeName; private String crimeName;
/** 刑期 */ /** 刑期 */
@Excel(name = "刑期", sort = 9) @Excel(name = "刑期", sort = 10)
private String sentenceTerm; private String sentenceTerm;
/** 刑期起日 */ /** 刑期起日 */
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "刑期起日", sort = 10, width = 30, dateFormat = "yyyy-MM-dd") @Excel(name = "刑期起日", sort = 11, width = 30, dateFormat = "yyyy-MM-dd")
private java.util.Date sentenceStartDate; private java.util.Date sentenceStartDate;
/** 刑期止日 */ /** 刑期止日 */
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "刑期止日", sort = 11, width = 30, dateFormat = "yyyy-MM-dd") @Excel(name = "刑期止日", sort = 12, width = 30, dateFormat = "yyyy-MM-dd")
private java.util.Date sentenceEndDate; private java.util.Date sentenceEndDate;
/** 入监时间 */ /** 入监时间 */
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "入监时间", sort = 12, width = 30, dateFormat = "yyyy-MM-dd") @Excel(name = "入监时间", sort = 13, width = 30, dateFormat = "yyyy-MM-dd")
private java.util.Date entryDate; private java.util.Date entryDate;
/** 用户状态0在押 1释放 2外出 3假释 */ /** 用户状态0在押 1释放 2外出 3假释 */
@Excel(name = "状态", sort = 13, readConverterExp = "0=在押,1=释放,2=外出,3=假释") @Excel(name = "状态", sort = 14, readConverterExp = "0=在押,1=释放,2=外出,3=假释")
private String status; private String status;
/** 部门ID */ /** 部门ID */
@ -98,7 +99,7 @@ public class PsyUserProfile extends BaseEntity
private String deptName; private String deptName;
/** 信息编号 */ /** 信息编号 */
@Excel(name = "信息编号", sort = 1) @Excel(name = "信息编号(必填)", sort = 1)
private String infoNumber; private String infoNumber;
public Long getProfileId() public Long getProfileId()

View File

@ -91,5 +91,12 @@ public interface PsyScalePermissionMapper
* @return 结果 * @return 结果
*/ */
public int deletePermissionByScaleId(Long scaleId); public int deletePermissionByScaleId(Long scaleId);
/**
* 查询所有已配置权限的量表ID
*
* @return 量表ID集合
*/
public List<Long> selectAllScaleIdsWithPermission();
} }

View File

@ -9,8 +9,10 @@ import org.springframework.transaction.annotation.Transactional;
import com.ddnai.common.utils.SecurityUtils; import com.ddnai.common.utils.SecurityUtils;
import com.ddnai.system.domain.psychology.PsyScalePermission; import com.ddnai.system.domain.psychology.PsyScalePermission;
import com.ddnai.system.mapper.psychology.PsyScalePermissionMapper; import com.ddnai.system.mapper.psychology.PsyScalePermissionMapper;
import com.ddnai.system.domain.psychology.PsyQuestionnaire;
import com.ddnai.system.service.psychology.IPsyScalePermissionService; import com.ddnai.system.service.psychology.IPsyScalePermissionService;
import com.ddnai.system.service.psychology.IPsyScaleService; import com.ddnai.system.service.psychology.IPsyScaleService;
import com.ddnai.system.service.psychology.IPsyQuestionnaireService;
/** /**
* 量表权限 服务层实现 * 量表权限 服务层实现
@ -28,6 +30,9 @@ public class PsyScalePermissionServiceImpl implements IPsyScalePermissionService
@Autowired @Autowired
private IPsyScaleService scaleService; private IPsyScaleService scaleService;
@Autowired
private IPsyQuestionnaireService questionnaireService;
/** /**
* 查询量表权限信息 * 查询量表权限信息
* *
@ -92,15 +97,7 @@ public class PsyScalePermissionServiceImpl implements IPsyScalePermissionService
@Override @Override
public int insertPermission(PsyScalePermission permission) public int insertPermission(PsyScalePermission permission)
{ {
// 验证 scale_id 是否存在 validateScaleExists(permission.getScaleId());
if (permission.getScaleId() != null)
{
com.ddnai.system.domain.psychology.PsyScale scale = scaleService.selectScaleById(permission.getScaleId());
if (scale == null)
{
throw new RuntimeException("量表不存在scaleId: " + permission.getScaleId());
}
}
if (permission.getStatus() == null) if (permission.getStatus() == null)
{ {
@ -150,15 +147,7 @@ public class PsyScalePermissionServiceImpl implements IPsyScalePermissionService
@Override @Override
public int updatePermission(PsyScalePermission permission) public int updatePermission(PsyScalePermission permission)
{ {
// 验证 scale_id 是否存在 validateScaleExists(permission.getScaleId());
if (permission.getScaleId() != null)
{
com.ddnai.system.domain.psychology.PsyScale scale = scaleService.selectScaleById(permission.getScaleId());
if (scale == null)
{
throw new RuntimeException("量表不存在scaleId: " + permission.getScaleId());
}
}
return permissionMapper.updatePermission(permission); return permissionMapper.updatePermission(permission);
} }
@ -239,12 +228,10 @@ public class PsyScalePermissionServiceImpl implements IPsyScalePermissionService
} }
try { try {
// 验证 scale_id 是否存在 if (!validateScaleExistsSilent(scaleId))
com.ddnai.system.domain.psychology.PsyScale scale = scaleService.selectScaleById(scaleId);
if (scale == null)
{ {
log.warn("量表不存在跳过该权限分配scaleId: {}", scaleId); log.warn("量表/问卷不存在跳过该权限分配scaleId: {}", scaleId);
continue; // 跳过不存在的量表 continue;
} }
PsyScalePermission permission = new PsyScalePermission(); PsyScalePermission permission = new PsyScalePermission();
@ -267,5 +254,35 @@ public class PsyScalePermissionServiceImpl implements IPsyScalePermissionService
} }
return count; return count;
} }
@Override
public List<Long> selectAllScaleIdsWithPermission()
{
return permissionMapper.selectAllScaleIdsWithPermission();
}
private void validateScaleExists(Long scaleId)
{
if (!validateScaleExistsSilent(scaleId))
{
throw new RuntimeException("量表或问卷不存在scaleId: " + scaleId);
}
}
private boolean validateScaleExistsSilent(Long scaleId)
{
if (scaleId == null)
{
return true;
}
if (scaleId < 0)
{
Long questionnaireId = Math.abs(scaleId);
PsyQuestionnaire questionnaire = questionnaireService.selectQuestionnaireById(questionnaireId);
return questionnaire != null;
}
com.ddnai.system.domain.psychology.PsyScale scale = scaleService.selectScaleById(scaleId);
return scale != null;
}
} }

View File

@ -14,10 +14,12 @@ import com.ddnai.common.utils.SecurityUtils;
import com.ddnai.common.utils.StringUtils; import com.ddnai.common.utils.StringUtils;
import com.ddnai.system.domain.psychology.PsyUserProfile; import com.ddnai.system.domain.psychology.PsyUserProfile;
import com.ddnai.system.mapper.psychology.PsyUserProfileMapper; import com.ddnai.system.mapper.psychology.PsyUserProfileMapper;
import com.ddnai.system.domain.dto.ImportProgress;
import com.ddnai.system.service.ISysConfigService; import com.ddnai.system.service.ISysConfigService;
import com.ddnai.system.service.ISysRoleService; import com.ddnai.system.service.ISysRoleService;
import com.ddnai.system.service.ISysUserService; import com.ddnai.system.service.ISysUserService;
import com.ddnai.system.service.psychology.IPsyUserProfileService; import com.ddnai.system.service.psychology.IPsyUserProfileService;
import com.ddnai.system.service.psychology.support.ImportProgressManager;
/** /**
* 用户档案扩展表 服务层实现 * 用户档案扩展表 服务层实现
@ -41,6 +43,9 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
@Autowired @Autowired
private ISysRoleService roleService; private ISysRoleService roleService;
@Autowired
private ImportProgressManager importProgressManager;
/** /**
* 查询档案信息 * 查询档案信息
* *
@ -134,14 +139,14 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
profile.setInfoNumber(infoNumber); profile.setInfoNumber(infoNumber);
validateInfoNumberUnique(infoNumber, null); validateInfoNumberUnique(infoNumber, null);
// 验证姓名如果提供只能包含汉字 // 验证姓名如果提供只能包含汉字和数字
if (StringUtils.isNotEmpty(profile.getUserName())) if (StringUtils.isNotEmpty(profile.getUserName()))
{ {
String userName = profile.getUserName().trim(); String userName = profile.getUserName().trim();
if (!userName.matches("^[\\u4e00-\\u9fa5]+$")) if (!userName.matches("^[\\u4e00-\\u9fa5\\d]+$"))
{ {
log.error("创建用户档案失败:姓名格式错误,只能输入汉字userName: {}", userName); log.error("创建用户档案失败:姓名格式错误,只能输入汉字和数字userName: {}", userName);
throw new ServiceException("姓名只能输入汉字"); throw new ServiceException("姓名只能输入汉字和数字");
} }
profile.setUserName(userName); profile.setUserName(userName);
} }
@ -302,14 +307,14 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
profile.setInfoNumber(infoNumber); profile.setInfoNumber(infoNumber);
validateInfoNumberUnique(infoNumber, profile.getProfileId()); validateInfoNumberUnique(infoNumber, profile.getProfileId());
// 验证姓名如果提供只能包含汉字 // 验证姓名如果提供只能包含汉字和数字
if (StringUtils.isNotEmpty(profile.getUserName())) if (StringUtils.isNotEmpty(profile.getUserName()))
{ {
String userName = profile.getUserName().trim(); String userName = profile.getUserName().trim();
if (!userName.matches("^[\\u4e00-\\u9fa5]+$")) if (!userName.matches("^[\\u4e00-\\u9fa5\\d]+$"))
{ {
log.error("修改用户档案失败:姓名格式错误,只能输入汉字userName: {}", userName); log.error("修改用户档案失败:姓名格式错误,只能输入汉字和数字userName: {}", userName);
throw new ServiceException("姓名只能输入汉字"); throw new ServiceException("姓名只能输入汉字和数字");
} }
profile.setUserName(userName); profile.setUserName(userName);
syncUserName(profile.getUserId(), userName); syncUserName(profile.getUserId(), userName);
@ -488,11 +493,14 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
{ {
throw new ServiceException("导入用户档案数据不能为空!"); throw new ServiceException("导入用户档案数据不能为空!");
} }
final String progressKey = importProgressManager.buildProfileKey(operName);
importProgressManager.start(progressKey, profileList.size());
int successNum = 0; int successNum = 0;
int failureNum = 0; int failureNum = 0;
StringBuilder successMsg = new StringBuilder(); StringBuilder successMsg = new StringBuilder();
StringBuilder failureMsg = new StringBuilder(); StringBuilder failureMsg = new StringBuilder();
try
{
for (PsyUserProfile profile : profileList) for (PsyUserProfile profile : profileList)
{ {
try try
@ -505,6 +513,7 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
{ {
failureNum++; failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、档案信息编号为空"); failureMsg.append("<br/>").append(failureNum).append("、档案信息编号为空");
importProgressManager.recordFailure(progressKey);
continue; continue;
} }
@ -516,6 +525,7 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
this.insertProfile(profile); this.insertProfile(profile);
successNum++; successNum++;
successMsg.append("<br/>").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 导入成功"); successMsg.append("<br/>").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 导入成功");
importProgressManager.recordSuccess(progressKey);
} }
else if (isUpdateSupport) else if (isUpdateSupport)
{ {
@ -526,11 +536,13 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
this.updateProfile(profile); this.updateProfile(profile);
successNum++; successNum++;
successMsg.append("<br/>").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 更新成功"); successMsg.append("<br/>").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 更新成功");
importProgressManager.recordSuccess(progressKey);
} }
else else
{ {
failureNum++; failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 已存在"); failureMsg.append("<br/>").append(failureNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 已存在");
importProgressManager.recordFailure(progressKey);
} }
} }
catch (Exception e) catch (Exception e)
@ -538,21 +550,25 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
failureNum++; failureNum++;
String msg = "<br/>" + failureNum + "、信息编号 " + profile.getInfoNumber() + " 导入失败:"; String msg = "<br/>" + failureNum + "、信息编号 " + profile.getInfoNumber() + " 导入失败:";
failureMsg.append(msg).append(e.getMessage()); failureMsg.append(msg).append(e.getMessage());
importProgressManager.recordFailure(progressKey);
log.error(msg, e); log.error(msg, e);
} }
} }
// 根据成功/失败情况生成结果
if (failureNum > 0) if (failureNum > 0)
{ {
if (successNum == 0) if (successNum == 0)
{ {
// 全部失败保持原有行为抛出异常 // 全部失败
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:"); failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
throw new ServiceException(failureMsg.toString()); String finalMsg = failureMsg.toString();
importProgressManager.finishFailure(progressKey, finalMsg);
throw new ServiceException(finalMsg);
} }
else else
{ {
// 部分成功部分失败同时返回成功和失败统计前端可一次性展示 // 部分成功部分失败同时返回成功和失败统计
StringBuilder resultMsg = new StringBuilder(); StringBuilder resultMsg = new StringBuilder();
resultMsg.append("本次导入完成:共 ") resultMsg.append("本次导入完成:共 ")
.append(successNum + failureNum) .append(successNum + failureNum)
@ -560,20 +576,50 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
.append(successNum) .append(successNum)
.append(" 条,失败 ") .append(" 条,失败 ")
.append(failureNum) .append(failureNum)
.append(" 条。成功明细如下:"); .append(" 条。");
if (successNum > 0)
{
resultMsg.append("<br/>成功明细如下:");
resultMsg.append(successMsg); resultMsg.append(successMsg);
}
failureMsg.insert(0, "<br/><br/>失败明细如下:"); failureMsg.insert(0, "<br/><br/>失败明细如下:");
resultMsg.append(failureMsg); resultMsg.append(failureMsg);
return resultMsg.toString();
} String finalMsg = resultMsg.toString();
} importProgressManager.finishFailure(progressKey, finalMsg);
else return finalMsg;
{ }
// 全部成功 }
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
return successMsg.toString(); // 没有失败
} if (successNum > 0)
{
String successMessage = "恭喜您,数据已全部导入成功!<br/>" + successMsg.toString();
importProgressManager.finishSuccess(progressKey, successMessage);
return successMessage;
}
// 理论上不会到这里既没有成功也没有失败但为安全起见保留兜底逻辑
importProgressManager.finishSuccess(progressKey, "导入完成,但未检测到需要处理的数据。");
return "导入完成,但未检测到需要处理的数据。";
}
catch (RuntimeException ex)
{
if (!(ex instanceof ServiceException))
{
importProgressManager.finishFailure(progressKey, ex.getMessage());
}
throw ex;
}
}
@Override
public ImportProgress getImportProgress(String operName)
{
String progressKey = importProgressManager.buildProfileKey(operName);
return importProgressManager.snapshot(progressKey);
} }
} }

View File

@ -83,6 +83,13 @@ public interface IPsyScalePermissionService
*/ */
public int deletePermissionByScaleId(Long scaleId); public int deletePermissionByScaleId(Long scaleId);
/**
* 查询所有已配置权限的量表ID
*
* @return 量表ID集合
*/
public List<Long> selectAllScaleIdsWithPermission();
/** /**
* 批量分配用户量表权限 * 批量分配用户量表权限
* *

View File

@ -1,6 +1,7 @@
package com.ddnai.system.service.psychology; package com.ddnai.system.service.psychology;
import java.util.List; import java.util.List;
import com.ddnai.system.domain.dto.ImportProgress;
import com.ddnai.system.domain.psychology.PsyUserProfile; import com.ddnai.system.domain.psychology.PsyUserProfile;
/** /**
@ -83,5 +84,13 @@ public interface IPsyUserProfileService
* @return 结果 * @return 结果
*/ */
public String importProfile(List<PsyUserProfile> profileList, Boolean isUpdateSupport, String operName); public String importProfile(List<PsyUserProfile> profileList, Boolean isUpdateSupport, String operName);
/**
* 查询导入进度
*
* @param operName 操作人
* @return 进度信息
*/
public ImportProgress getImportProgress(String operName);
} }

View File

@ -0,0 +1,130 @@
package com.ddnai.system.service.psychology.support;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import org.springframework.stereotype.Component;
import com.ddnai.system.domain.dto.ImportProgress;
/**
* 导入进度管理
*/
@Component
public class ImportProgressManager
{
private static final long EXPIRE_MILLIS = TimeUnit.MINUTES.toMillis(30);
private final Map<String, ImportProgress> cache = new ConcurrentHashMap<>();
public String buildProfileKey(String username)
{
return "psychology_profile:" + username;
}
public void start(String key, int total)
{
if (Objects.isNull(key))
{
return;
}
ImportProgress progress = new ImportProgress();
progress.setTotal(total);
progress.setProcessed(0);
progress.setSuccess(0);
progress.setFailure(0);
progress.setStatus("processing");
progress.setStartTime(LocalDateTime.now());
progress.setLastUpdateTime(LocalDateTime.now());
progress.setExpireAt(System.currentTimeMillis() + EXPIRE_MILLIS);
cache.put(key, progress);
}
public void recordSuccess(String key)
{
adjustProgress(key, true);
}
public void recordFailure(String key)
{
adjustProgress(key, false);
}
private void adjustProgress(String key, boolean success)
{
ImportProgress progress = cache.get(key);
if (progress == null)
{
return;
}
progress.setProcessed(progress.getProcessed() + 1);
if (success)
{
progress.setSuccess(progress.getSuccess() + 1);
}
else
{
progress.setFailure(progress.getFailure() + 1);
}
refreshMeta(progress);
}
public void finishSuccess(String key, String message)
{
ImportProgress progress = cache.get(key);
if (progress == null)
{
return;
}
progress.setStatus("success");
progress.setMessage(message);
progress.setProcessed(progress.getTotal());
refreshMeta(progress);
}
public void finishFailure(String key, String message)
{
ImportProgress progress = cache.get(key);
if (progress == null)
{
return;
}
progress.setStatus("failed");
progress.setMessage(message);
refreshMeta(progress);
}
private void refreshMeta(ImportProgress progress)
{
progress.setLastUpdateTime(LocalDateTime.now());
progress.setExpireAt(System.currentTimeMillis() + EXPIRE_MILLIS);
}
public ImportProgress snapshot(String key)
{
ImportProgress progress = cache.get(key);
if (progress == null)
{
return null;
}
long now = System.currentTimeMillis();
if (progress.isExpired(now))
{
cache.remove(key);
return null;
}
return progress.copy();
}
public void clear(String key)
{
if (key != null)
{
cache.remove(key);
}
}
}

View File

@ -77,7 +77,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectPausedAssessmentList" parameterType="Long" resultMap="PsyAssessmentResult"> <select id="selectPausedAssessmentList" parameterType="Long" resultMap="PsyAssessmentResult">
<include refid="selectAssessmentVo"/> <include refid="selectAssessmentVo"/>
where a.user_id = #{userId} and a.status = '3' <where>
a.status = '3'
<if test="userId != null">
and a.user_id = #{userId}
</if>
</where>
order by a.pause_time desc order by a.pause_time desc
</select> </select>
@ -349,4 +354,3 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select> </select>
</mapper> </mapper>

View File

@ -26,6 +26,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateBy" column="update_by" /> <result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" /> <result property="updateTime" column="update_time" />
<result property="remark" column="remark" /> <result property="remark" column="remark" />
<result property="sourceType" column="source_type" />
</resultMap> </resultMap>
<sql id="selectScaleVo"> <sql id="selectScaleVo">
@ -34,7 +35,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
COALESCE(COUNT(i.item_id), 0) as item_count, COALESCE(COUNT(i.item_id), 0) as item_count,
s.estimated_time, s.target_population, s.estimated_time, s.target_population,
s.author, s.source, s.reference, s.status, s.sort_order, s.create_by, s.create_time, s.author, s.source, s.reference, s.status, s.sort_order, s.create_by, s.create_time,
s.update_by, s.update_time, s.remark s.update_by, s.update_time, s.remark,
'scale' as source_type
from psy_scale s from psy_scale s
left join psy_scale_item i on s.scale_id = i.scale_id left join psy_scale_item i on s.scale_id = i.scale_id
</sql> </sql>

View File

@ -20,6 +20,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateTime" column="update_time" /> <result property="updateTime" column="update_time" />
<result property="remark" column="remark" /> <result property="remark" column="remark" />
<result property="scaleName" column="scale_name" /> <result property="scaleName" column="scale_name" />
<result property="sourceType" column="source_type" />
<result property="deptName" column="dept_name" /> <result property="deptName" column="dept_name" />
<result property="roleName" column="role_name" /> <result property="roleName" column="role_name" />
<result property="userName" column="user_name" /> <result property="userName" column="user_name" />
@ -28,9 +29,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<sql id="selectPermissionVo"> <sql id="selectPermissionVo">
select p.permission_id, p.scale_id, p.dept_id, p.role_id, p.user_id, p.class_name, select p.permission_id, p.scale_id, p.dept_id, p.role_id, p.user_id, p.class_name,
p.start_time, p.end_time, p.status, p.create_by, p.create_time, p.update_by, p.update_time, p.remark, p.start_time, p.end_time, p.status, p.create_by, p.create_time, p.update_by, p.update_time, p.remark,
s.scale_name, d.dept_name, r.role_name, u.user_name case when p.scale_id &lt; 0 then q.questionnaire_name else s.scale_name end as scale_name,
case when p.scale_id &lt; 0 then 'questionnaire' else 'scale' end as source_type,
d.dept_name, r.role_name, u.user_name
from psy_scale_permission p from psy_scale_permission p
left join psy_scale s on p.scale_id = s.scale_id left join psy_scale s on p.scale_id = s.scale_id
left join psy_questionnaire q on q.questionnaire_id = -p.scale_id
left join sys_dept d on p.dept_id = d.dept_id left join sys_dept d on p.dept_id = d.dept_id
left join sys_role r on p.role_id = r.role_id left join sys_role r on p.role_id = r.role_id
left join sys_user u on p.user_id = u.user_id left join sys_user u on p.user_id = u.user_id
@ -175,4 +179,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
delete from psy_scale_permission where scale_id = #{scaleId} delete from psy_scale_permission where scale_id = #{scaleId}
</delete> </delete>
<select id="selectAllScaleIdsWithPermission" resultType="Long">
select distinct scale_id
from psy_scale_permission
where status = '0'
</select>
</mapper> </mapper>

View File

@ -109,6 +109,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="crimeName != null and crimeName != ''"> <if test="crimeName != null and crimeName != ''">
AND p.crime_name like concat('%', #{crimeName}, '%') AND p.crime_name like concat('%', #{crimeName}, '%')
</if> </if>
<if test="status != null and status != ''">
AND u.status = #{status}
</if>
<if test="params.beginTime != null and params.beginTime != ''"> <if test="params.beginTime != null and params.beginTime != ''">
AND date_format(u.create_time,'%Y%m%d') &gt;= date_format(#{params.beginTime},'%Y%m%d') AND date_format(u.create_time,'%Y%m%d') &gt;= date_format(#{params.beginTime},'%Y%m%d')
</if> </if>

View File

@ -19,10 +19,11 @@ export function myAssessmentList(query) {
} }
// 查询暂停的测评列表 // 查询暂停的测评列表
export function pausedAssessmentList() { export function pausedAssessmentList(params) {
return request({ return request({
url: '/psychology/assessment/pausedList', url: '/psychology/assessment/pausedList',
method: 'get' method: 'get',
params: params
}) })
} }

View File

@ -102,3 +102,11 @@ export function delUserInProfile(userIds) {
}) })
} }
// 查询档案导入进度
export function getProfileImportProgress() {
return request({
url: '/psychology/profile/importProgress',
method: 'get'
})
}

View File

@ -235,7 +235,7 @@ export const dynamicRoutes = [
meta: { meta: {
title: '心理测评管理', title: '心理测评管理',
icon: 'chart', icon: 'chart',
roles: ['admin'] roles: ['admin', 'teacher']
}, },
children: [ children: [
// 量表管理 // 量表管理
@ -246,7 +246,7 @@ export const dynamicRoutes = [
meta: { meta: {
title: '量表管理', title: '量表管理',
icon: 'table', icon: 'table',
roles: ['admin'] roles: ['admin', 'teacher']
} }
}, },
// 题目管理(隐藏菜单,通过量表管理页面进入) // 题目管理(隐藏菜单,通过量表管理页面进入)
@ -257,7 +257,7 @@ export const dynamicRoutes = [
hidden: true, hidden: true,
meta: { meta: {
title: '题目管理', title: '题目管理',
roles: ['admin'] roles: ['admin', 'teacher']
} }
}, },
// 因子管理(隐藏菜单,通过量表管理页面进入) // 因子管理(隐藏菜单,通过量表管理页面进入)
@ -268,7 +268,7 @@ export const dynamicRoutes = [
hidden: true, hidden: true,
meta: { meta: {
title: '因子管理', title: '因子管理',
roles: ['admin'] roles: ['admin', 'teacher']
} }
}, },
// 测评管理 // 测评管理
@ -279,7 +279,7 @@ export const dynamicRoutes = [
meta: { meta: {
title: '测评管理', title: '测评管理',
icon: 'edit', icon: 'edit',
roles: ['admin'] roles: ['admin', 'teacher']
} }
}, },
{ {
@ -289,7 +289,7 @@ export const dynamicRoutes = [
meta: { meta: {
title: '全员测评分析', title: '全员测评分析',
icon: 'chart', icon: 'chart',
roles: ['admin'] roles: ['admin', 'teacher']
} }
}, },
// 开始测评 // 开始测评
@ -300,7 +300,7 @@ export const dynamicRoutes = [
hidden: true, hidden: true,
meta: { meta: {
title: '开始测评', title: '开始测评',
roles: ['admin'] roles: ['admin', 'teacher']
} }
}, },
// 测评进行中 // 测评进行中
@ -311,7 +311,7 @@ export const dynamicRoutes = [
hidden: true, hidden: true,
meta: { meta: {
title: '测评中', title: '测评中',
roles: ['admin'] roles: ['admin', 'teacher']
} }
}, },
// 测评报告 // 测评报告
@ -322,7 +322,7 @@ export const dynamicRoutes = [
hidden: true, hidden: true,
meta: { meta: {
title: '测评报告', title: '测评报告',
roles: ['admin'] roles: ['admin', 'teacher']
} }
}, },
// 报告管理 // 报告管理
@ -333,7 +333,7 @@ export const dynamicRoutes = [
meta: { meta: {
title: '报告管理', title: '报告管理',
icon: 'document', icon: 'document',
roles: ['admin'] roles: ['admin', 'teacher']
} }
}, },
// 报告详情 // 报告详情
@ -344,7 +344,7 @@ export const dynamicRoutes = [
hidden: true, hidden: true,
meta: { meta: {
title: '报告详情', title: '报告详情',
roles: ['admin'] roles: ['admin', 'teacher']
} }
}, },
// 综合评估 // 综合评估
@ -355,7 +355,7 @@ export const dynamicRoutes = [
meta: { meta: {
title: '综合评估', title: '综合评估',
icon: 'chart', icon: 'chart',
roles: ['admin'] roles: ['admin', 'teacher']
} }
}, },
// 量表权限管理 // 量表权限管理
@ -366,7 +366,7 @@ export const dynamicRoutes = [
meta: { meta: {
title: '量表权限管理', title: '量表权限管理',
icon: 'lock', icon: 'lock',
roles: ['admin'] roles: ['admin', 'teacher']
} }
}, },
// 用户量表权限分配 // 用户量表权限分配
@ -377,7 +377,7 @@ export const dynamicRoutes = [
hidden: true, hidden: true,
meta: { meta: {
title: '分配量表权限', title: '分配量表权限',
roles: ['admin'] roles: ['admin', 'teacher']
} }
}, },
// 解释配置 // 解释配置
@ -388,7 +388,7 @@ export const dynamicRoutes = [
meta: { meta: {
title: '解释配置', title: '解释配置',
icon: 'config', icon: 'config',
roles: ['admin'] roles: ['admin', 'teacher']
} }
}, },
// 用户档案 // 用户档案
@ -399,7 +399,7 @@ export const dynamicRoutes = [
meta: { meta: {
title: '用户档案', title: '用户档案',
icon: 'user', icon: 'user',
roles: ['admin'] roles: ['admin', 'teacher']
} }
}, },
// 自定义问卷 // 自定义问卷
@ -410,7 +410,7 @@ export const dynamicRoutes = [
meta: { meta: {
title: '自定义问卷', title: '自定义问卷',
icon: 'edit', icon: 'edit',
roles: ['admin'] roles: ['admin', 'teacher']
} }
}, },
// 问卷开始答题 // 问卷开始答题
@ -443,7 +443,7 @@ export const dynamicRoutes = [
hidden: true, hidden: true,
meta: { meta: {
title: '问卷题目管理', title: '问卷题目管理',
roles: ['admin'] roles: ['admin', 'teacher']
} }
}, },
// 主观题评分 // 主观题评分
@ -454,7 +454,7 @@ export const dynamicRoutes = [
meta: { meta: {
title: '主观题评分', title: '主观题评分',
icon: 'edit', icon: 'edit',
roles: ['admin'] roles: ['admin', 'teacher']
} }
} }
] ]

View File

@ -100,6 +100,9 @@ service.interceptors.response.use(res => {
}) })
} }
return Promise.reject('无效的会话,或者会话已过期,请重新登录。') return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 403) {
// 403权限不足错误静默处理不显示提示
return Promise.reject('no_permission')
} else if (code === 500) { } else if (code === 500) {
Message({ message: msg, type: 'error' }) Message({ message: msg, type: 'error' })
return Promise.reject(new Error(msg)) return Promise.reject(new Error(msg))
@ -116,6 +119,12 @@ service.interceptors.response.use(res => {
async error => { async error => {
console.log('err' + error) console.log('err' + error)
let { message } = error let { message } = error
// 403权限错误静默处理
if (error.response && error.response.status === 403) {
return Promise.reject('no_permission')
}
// 对于blob类型的错误响应尝试解析错误信息 // 对于blob类型的错误响应尝试解析错误信息
if (error.response && error.response.config && if (error.response && error.response.config &&
(error.response.config.responseType === 'blob' || error.response.config.responseType === 'arraybuffer') && (error.response.config.responseType === 'blob' || error.response.config.responseType === 'arraybuffer') &&

View File

@ -41,7 +41,7 @@
icon="el-icon-plus" icon="el-icon-plus"
size="mini" size="mini"
@click="handleStartAssessment" @click="handleStartAssessment"
v-hasPermi="['psychology:assessment:add']" v-hasPermi="['psychology:assessment:start']"
>开始测评</el-button> >开始测评</el-button>
</el-col> </el-col>
<el-col :span="1.5"> <el-col :span="1.5">
@ -61,7 +61,11 @@
<el-table v-loading="loading" :data="assessmentList" @selection-change="handleSelectionChange"> <el-table v-loading="loading" :data="assessmentList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" /> <el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" prop="assessmentId" width="80" /> <el-table-column label="序号" align="center" prop="assessmentId" width="80" />
<el-table-column label="量表ID" align="center" prop="scaleId" width="100" /> <el-table-column label="量表名称" align="center" prop="scaleName" min-width="150" :show-overflow-tooltip="true">
<template slot-scope="scope">
<span>{{ scope.row.scaleName || (scope.row.scaleId ? ('量表#' + scope.row.scaleId) : '-') }}</span>
</template>
</el-table-column>
<el-table-column label="被测评人" align="center" prop="assesseeName" width="120" /> <el-table-column label="被测评人" align="center" prop="assesseeName" width="120" />
<el-table-column label="开始时间" align="center" prop="startTime" width="180"> <el-table-column label="开始时间" align="center" prop="startTime" width="180">
<template slot-scope="scope"> <template slot-scope="scope">
@ -147,7 +151,9 @@
<el-descriptions :column="2" border style="margin-bottom: 20px;"> <el-descriptions :column="2" border style="margin-bottom: 20px;">
<el-descriptions-item label="测评ID">{{ currentAssessment.assessmentId }}</el-descriptions-item> <el-descriptions-item label="测评ID">{{ currentAssessment.assessmentId }}</el-descriptions-item>
<el-descriptions-item label="被测评人">{{ currentAssessment.assesseeName }}</el-descriptions-item> <el-descriptions-item label="被测评人">{{ currentAssessment.assesseeName }}</el-descriptions-item>
<el-descriptions-item label="量表ID">{{ currentAssessment.scaleId }}</el-descriptions-item> <el-descriptions-item label="量表名称">
{{ currentAssessment.scaleName || (currentAssessment.scaleId ? ('量表#' + currentAssessment.scaleId) : '-') }}
</el-descriptions-item>
<el-descriptions-item label="提交时间"> <el-descriptions-item label="提交时间">
<span v-if="currentAssessment.submitTime">{{ parseTime(currentAssessment.submitTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span> <span v-if="currentAssessment.submitTime">{{ parseTime(currentAssessment.submitTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
<span v-else>-</span> <span v-else>-</span>
@ -158,7 +164,6 @@
<el-divider content-position="left">答题详情</el-divider> <el-divider content-position="left">答题详情</el-divider>
<el-table :data="answerDetailList" border style="width: 100%"> <el-table :data="answerDetailList" border style="width: 100%">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="题目序号" prop="itemNumber" width="100" align="center" /> <el-table-column label="题目序号" prop="itemNumber" width="100" align="center" />
<el-table-column label="题目内容" prop="itemContent" min-width="200" :show-overflow-tooltip="true" /> <el-table-column label="题目内容" prop="itemContent" min-width="200" :show-overflow-tooltip="true" />
<el-table-column label="题目类型" prop="itemType" width="100" align="center"> <el-table-column label="题目类型" prop="itemType" width="100" align="center">

View File

@ -110,6 +110,11 @@ export default {
} }
}; };
}, },
watch: {
'form.profileId'(newVal) {
this.refreshPausedListByProfile(newVal)
}
},
created() { created() {
// URLscaleIdprofileId/userId // URLscaleIdprofileId/userId
const scaleId = this.$route.query.scaleId; const scaleId = this.$route.query.scaleId;
@ -130,7 +135,8 @@ export default {
this.loadScales(); this.loadScales();
this.loadProfiles(); this.loadProfiles();
this.loadPaused(); const initialPausedUserId = this.targetUserId ? parseInt(this.targetUserId) : this.$store.getters.id;
this.loadPaused(initialPausedUserId);
}, },
methods: { methods: {
/** 加载量表列表 */ /** 加载量表列表 */
@ -139,8 +145,9 @@ export default {
const userId = this.$store.getters.id; const userId = this.$store.getters.id;
const roles = this.$store.getters.roles || []; const roles = this.$store.getters.roles || [];
// userId === 1 roles 'admin' // admin
const isAdmin = userId === 1 || (roles && roles.includes('admin')); //
const isAdmin = roles && roles.includes('admin');
// //
if (isAdmin) { if (isAdmin) {
@ -284,11 +291,30 @@ export default {
}) })
}, },
/** 加载暂停的测评 */ /** 加载暂停的测评 */
loadPaused() { loadPaused(targetUserId) {
pausedAssessmentList().then(response => { const loginUserId = this.$store.getters.id;
const params = {};
if (targetUserId && !isNaN(targetUserId) && Number(targetUserId) !== Number(loginUserId)) {
params.userId = targetUserId;
}
pausedAssessmentList(params).then(response => {
this.pausedList = response.data || []; this.pausedList = response.data || [];
}); });
}, },
refreshPausedListByProfile(profileId) {
if (!profileId) {
const fallbackUserId = this.targetUserId ? parseInt(this.targetUserId) : this.$store.getters.id;
this.loadPaused(fallbackUserId);
return;
}
const profile = this.profileList.find(p => p.profileId === profileId);
if (profile && profile.userId) {
this.loadPaused(profile.userId);
} else {
const fallbackUserId = this.targetUserId ? parseInt(this.targetUserId) : this.$store.getters.id;
this.loadPaused(fallbackUserId);
}
},
/** 获取用户档案验证规则(动态) */ /** 获取用户档案验证规则(动态) */
getProfileRules() { getProfileRules() {
// //
@ -374,6 +400,15 @@ export default {
return; return;
} }
//
const pausedRecord = this.pausedList.find(item => item.scaleId === this.form.scaleId && item.status === '3');
if (pausedRecord) {
this.$modal.confirm('检测到该用户在该量表有暂停的测评,是否继续上一条记录?').then(() => {
this.handleContinue(pausedRecord);
});
return;
}
// //
this.loading = true; this.loading = true;
// //

View File

@ -38,10 +38,11 @@
size="small" size="small"
@click="speakText(currentItem.itemContent)" @click="speakText(currentItem.itemContent)"
:disabled="!isTtsSupported" :disabled="!isTtsSupported"
class="tts-btn" :class="['tts-btn', isSpeaking ? 'speaking' : '']"
title="朗读题干" title="朗读题干"
> >
<img :src="voiceIcon" alt="朗读题干" class="tts-icon" /> <i :class="isSpeaking ? 'el-icon-video-pause' : 'el-icon-service'"
style="font-size: 18px; color: #409EFF;"></i>
</el-button> </el-button>
</div> </div>
@ -55,10 +56,11 @@
size="mini" size="mini"
@click="speakText(option.optionContent)" @click="speakText(option.optionContent)"
:disabled="!isTtsSupported" :disabled="!isTtsSupported"
class="option-tts-btn" :class="['option-tts-btn', isSpeaking ? 'speaking' : '']"
title="朗读选项" title="朗读选项"
> >
<img :src="voiceIcon" alt="朗读选项" class="tts-icon" /> <i :class="isSpeaking ? 'el-icon-video-pause' : 'el-icon-service'"
style="font-size: 16px; color: #409EFF;"></i>
</el-button> </el-button>
</div> </div>
</el-radio-group> </el-radio-group>
@ -74,10 +76,11 @@
size="mini" size="mini"
@click="speakText(option.optionContent)" @click="speakText(option.optionContent)"
:disabled="!isTtsSupported" :disabled="!isTtsSupported"
class="option-tts-btn" :class="['option-tts-btn', isSpeaking ? 'speaking' : '']"
title="朗读选项" title="朗读选项"
> >
<img :src="voiceIcon" alt="朗读选项" class="tts-icon" /> <i :class="isSpeaking ? 'el-icon-video-pause' : 'el-icon-service'"
style="font-size: 16px; color: #409EFF;"></i>
</el-button> </el-button>
</div> </div>
</el-checkbox-group> </el-checkbox-group>
@ -134,7 +137,8 @@ export default {
isTtsSupported: false, isTtsSupported: false,
synth: null, synth: null,
currentUtterance: null, currentUtterance: null,
voiceIcon voiceIcon,
isSpeaking: false
}; };
}, },
computed: { computed: {
@ -208,45 +212,62 @@ export default {
/** 朗读文本 */ /** 朗读文本 */
speakText(text) { speakText(text) {
if (!this.isTtsSupported || !text || !text.trim()) { if (!this.isTtsSupported || !text || !text.trim()) {
this.$message.warning('浏览器不支持语音播放功能');
return; return;
} }
//
if (this.isSpeaking) {
this.stopSpeaking();
return;
}
this.stopSpeaking(); this.stopSpeaking();
this.currentUtterance = new SpeechSynthesisUtterance(text.trim()); this.currentUtterance = new SpeechSynthesisUtterance(text.trim());
this.currentUtterance.lang = 'zh-CN'; this.currentUtterance.lang = 'zh-CN';
this.currentUtterance.volume = 1.0; this.currentUtterance.volume = 1.0; //
this.currentUtterance.rate = 1.0; this.currentUtterance.rate = 0.9; //
this.currentUtterance.pitch = 1.0; this.currentUtterance.pitch = 1.0;
//
const voices = this.synth.getVoices(); const voices = this.synth.getVoices();
const chineseVoice = voices.find(voice => voice.lang.includes('zh') || voice.lang.includes('CN')); //
const chineseVoice = voices.find(voice =>
voice.lang.includes('zh') ||
voice.lang.includes('CN') ||
voice.name.includes('中文') ||
voice.name.includes('Chinese')
);
if (chineseVoice) { if (chineseVoice) {
this.currentUtterance.voice = chineseVoice; this.currentUtterance.voice = chineseVoice;
} }
let hasStarted = false;
this.currentUtterance.onstart = () => { this.currentUtterance.onstart = () => {
hasStarted = true; this.isSpeaking = true;
}; };
this.currentUtterance.onend = () => {
this.isSpeaking = false;
};
this.currentUtterance.onerror = (event) => { this.currentUtterance.onerror = (event) => {
console.error('TTS 错误:', event); this.isSpeaking = false;
if (hasStarted) { const errorType = event.error || '';
//
const ignoredErrors = ['interrupted', 'canceled'];
if (ignoredErrors.includes(errorType)) {
return; return;
} }
const errorType = event.error || ''; console.error('TTS 错误:', event);
const seriousErrors = ['network-error', 'synthesis-failed', 'synthesis-unavailable', 'not-allowed']; //
if (seriousErrors.includes(errorType)) {
this.$message.error('语音朗读失败:' + this.getErrorMessage(errorType));
} else if (errorType === 'text-too-long') {
this.$message.warning('文本过长,无法朗读');
} else if (errorType) {
this.$message.error('语音朗读失败');
}
}; };
try { try {
//
this.synth.speak(this.currentUtterance); this.synth.speak(this.currentUtterance);
} catch (error) { } catch (error) {
this.isSpeaking = false;
console.error('调用 speak 失败:', error); console.error('调用 speak 失败:', error);
this.$message.error('语音朗读失败:无法启动语音合成');
} }
}, },
/** 停止朗读 */ /** 停止朗读 */
@ -255,6 +276,7 @@ export default {
this.synth.cancel(); this.synth.cancel();
} }
this.currentUtterance = null; this.currentUtterance = null;
this.isSpeaking = false;
}, },
/** 错误信息 */ /** 错误信息 */
getErrorMessage(errorType) { getErrorMessage(errorType) {
@ -442,10 +464,26 @@ export default {
/** 退出 */ /** 退出 */
handleExit() { handleExit() {
this.$modal.confirm('确定要退出测评吗?已答题目将会保存。').then(() => { this.$modal.confirm('确定要退出测评吗?已答题目将会保存。').then(() => {
this.loading = true;
//
setTimeout(() => {
// 退
pauseAssessment(this.assessmentId).then(() => {
this.loading = false;
this.$modal.msgSuccess("测评进度已保存");
// //
const roles = this.$store.getters.roles || []; const roles = this.$store.getters.roles || [];
const isStudent = roles.some(role => role === 'student' || role.includes('学员')); const isStudent = roles.some(role => role === 'student' || role.includes('学员'));
this.$router.push(isStudent ? '/student/tests' : '/psychology/assessment'); this.$router.push(isStudent ? '/student/tests' : '/psychology/assessment');
}).catch(error => {
console.error('保存测评进度失败:', error);
this.loading = false;
// 使退
const roles = this.$store.getters.roles || [];
const isStudent = roles.some(role => role === 'student' || role.includes('学员'));
this.$router.push(isStudent ? '/student/tests' : '/psychology/assessment');
});
}, 500); // 500ms
}); });
}, },
/** 提交测评 */ /** 提交测评 */
@ -790,5 +828,37 @@ export default {
border-radius: 4px; border-radius: 4px;
} }
/* 语音播放时的动画效果 */
.tts-btn.speaking, .option-tts-btn.speaking {
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
.tts-btn .el-icon-video-pause,
.option-tts-btn .el-icon-video-pause {
color: #E6A23C !important;
animation: rotate 2s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style> </style>

View File

@ -6,15 +6,34 @@
<el-option <el-option
v-for="scale in scaleList" v-for="scale in scaleList"
:key="scale.scaleId" :key="scale.scaleId"
:label="scale.scaleName" :label="formatScaleLabel(scale)"
:value="scale.scaleId"> :value="scale.scaleId">
<span class="scale-option">
<el-tag
size="mini"
:type="scale.sourceType === 'questionnaire' ? 'warning' : 'primary'"
style="margin-right: 6px;">
{{ scale.sourceType === 'questionnaire' ? '问卷' : '量表' }}
</el-tag>
<span>{{ scale.scaleName }}</span>
</span>
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="用户名称" prop="userId"> <el-form-item label="用户名称" prop="userId">
<el-select v-model="queryParams.userId" placeholder="请选择用户" clearable filterable style="width: 200px"> <el-select
v-model="queryParams.userId"
placeholder="请选择用户"
clearable
filterable
remote
:remote-method="searchUsers"
:loading="userSearchLoading"
style="width: 220px"
@focus="handleSelectFocus"
@visible-change="val => val && handleSelectFocus()">
<el-option <el-option
v-for="user in userList" v-for="user in userOptions"
:key="user.userId" :key="user.userId"
:label="user.nickName ? `${user.nickName}${user.userName}` : user.userName" :label="user.nickName ? `${user.nickName}${user.userName}` : user.userName"
:value="Number(user.userId)"> :value="Number(user.userId)">
@ -45,7 +64,17 @@
<el-table v-loading="loading" :data="permissionList" @selection-change="handleSelectionChange"> <el-table v-loading="loading" :data="permissionList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" /> <el-table-column type="selection" width="55" align="center" />
<el-table-column label="量表名称" align="center" prop="scaleName" width="200" /> <el-table-column label="量表/问卷" align="center" prop="scaleName" width="220">
<template slot-scope="scope">
<el-tag
size="mini"
:type="scope.row.sourceType === 'questionnaire' ? 'warning' : 'primary'"
style="margin-right: 6px;">
{{ scope.row.sourceType === 'questionnaire' ? '问卷' : '量表' }}
</el-tag>
<span>{{ scope.row.scaleName || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="用户名称" align="center" prop="userNames" min-width="250"> <el-table-column label="用户名称" align="center" prop="userNames" min-width="250">
<template slot-scope="scope"> <template slot-scope="scope">
<span v-if="scope.row.hasAllUsers"> <span v-if="scope.row.hasAllUsers">
@ -66,9 +95,10 @@
<span v-else style="color: #909399;">-</span> <span v-else style="color: #909399;">-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="部门名称" align="center" prop="deptName" width="150" /> <!-- 隐藏部门角色班级列 -->
<el-table-column label="角色名称" align="center" prop="roleName" width="150" /> <!-- <el-table-column label="部门名称" align="center" prop="deptName" width="150" /> -->
<el-table-column label="班级名称" align="center" prop="className" width="150" /> <!-- <el-table-column label="角色名称" align="center" prop="roleName" width="150" /> -->
<!-- <el-table-column label="班级名称" align="center" prop="className" width="150" /> -->
<el-table-column label="开始时间" align="center" prop="startTime" width="180"> <el-table-column label="开始时间" align="center" prop="startTime" width="180">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span> <span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
@ -102,39 +132,59 @@
/> />
<!-- 添加或修改权限对话框 --> <!-- 添加或修改权限对话框 -->
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body> <el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="120px"> <el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-form-item label="量表" prop="scaleId"> <el-form-item label="量表" prop="scaleId">
<el-select v-model="form.scaleId" placeholder="请选择量表" style="width: 100%;" filterable> <div style="border: 1px solid #DCDFE6; border-radius: 4px; padding: 10px; max-height: 300px; overflow-y: auto;">
<el-option <el-input
v-for="scale in scaleList" v-model="scaleSearchKeyword"
:key="scale.scaleId" placeholder="请输入量表名称搜索"
:label="scale.scaleName" clearable
:value="scale.scaleId"> size="small"
</el-option> style="margin-bottom: 10px;"
</el-select> prefix-icon="el-icon-search">
</el-input>
<el-table
:data="filteredScaleList"
@row-click="handleScaleRowClick"
highlight-current-row
max-height="250">
<el-table-column width="55" align="center">
<template slot-scope="scope">
<el-radio
:label="scope.row.scaleId"
v-model="form.scaleId"
@change="handleScaleChange(scope.row)">
<span></span>
</el-radio>
</template>
</el-table-column>
<el-table-column label="类型" width="80" align="center">
<template slot-scope="scope">
<el-tag
size="mini"
:type="scope.row.sourceType === 'questionnaire' ? 'warning' : 'primary'">
{{ scope.row.sourceType === 'questionnaire' ? '问卷' : '量表' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="量表名称" prop="scaleName" :show-overflow-tooltip="true" />
<el-table-column label="量表编码" prop="scaleCode" width="150" />
<el-table-column label="题目数量" prop="itemCount" width="100" align="center" />
</el-table>
</div>
</el-form-item> </el-form-item>
<el-form-item label="用户" prop="userIds"> <el-form-item label="用户" prop="userIds">
<el-select <el-input
v-model="form.userIds" :value="selectedUserDisplay"
placeholder="请选择用户(可多选,留空表示所有用户)" placeholder="点击选择用户(留空表示所有用户)"
clearable readonly
filterable
multiple
reserve-keyword
:loading="userSearchLoading"
style="width: 100%;" style="width: 100%;"
@focus="handleSelectFocus" @focus="showUserSelectDialog = true">
@change="handleUserIdsChange"> <el-button slot="append" icon="el-icon-search" @click="showUserSelectDialog = true">选择</el-button>
<el-option </el-input>
v-for="user in userOptions"
:key="user.userId"
:label="user.nickName ? `${user.nickName}${user.userName}` : user.userName"
:value="user.userId">
</el-option>
</el-select>
<div style="margin-top: 5px; color: #909399; font-size: 12px;"> <div style="margin-top: 5px; color: #909399; font-size: 12px;">
提示可输入姓名或账号搜索支持多选留空表示所有用户 提示留空表示所有用户点击"选择"按钮可按信息编号姓名监区筛选用户
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="开始时间" prop="startTime"> <el-form-item label="开始时间" prop="startTime">
@ -170,6 +220,74 @@
<el-button @click="cancel"> </el-button> <el-button @click="cancel"> </el-button>
</div> </div>
</el-dialog> </el-dialog>
<!-- 用户选择对话框 -->
<el-dialog title="选择用户" :visible.sync="showUserSelectDialog" width="900px" append-to-body>
<el-form :model="userQueryParams" ref="userQueryForm" size="small" :inline="true">
<el-form-item label="信息编号" prop="infoNumber">
<el-input
v-model="userQueryParams.infoNumber"
placeholder="请输入信息编号"
clearable
@keyup.enter.native="handleUserQuery"
/>
</el-form-item>
<el-form-item label="姓名" prop="userName">
<el-input
v-model="userQueryParams.userName"
placeholder="请输入姓名"
clearable
@keyup.enter.native="handleUserQuery"
/>
</el-form-item>
<el-form-item label="监区" prop="prisonArea">
<el-select v-model="userQueryParams.prisonArea" placeholder="请选择监区" clearable filterable style="width: 200px">
<el-option
v-for="area in prisonAreaOptions"
:key="area"
:label="area"
:value="area">
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleUserQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetUserQuery">重置</el-button>
<el-button type="success" icon="el-icon-check" size="mini" @click="handleSelectAllUsers">全选所有用户</el-button>
<el-button type="warning" icon="el-icon-close" size="mini" @click="handleDeselectAllUsers">取消全选</el-button>
</el-form-item>
</el-form>
<el-table
ref="userTable"
:data="userSelectList"
@selection-change="handleUserSelectionChange"
@select-all="handleUserSelectAll"
height="400px"
v-loading="userSelectLoading">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column label="信息编号" prop="infoNumber" width="120" />
<el-table-column label="姓名" prop="userName" :show-overflow-tooltip="true" />
<el-table-column label="账号" prop="userAccount" :show-overflow-tooltip="true" />
<el-table-column label="监区" prop="prisonArea" :show-overflow-tooltip="true" />
<el-table-column label="状态" align="center" prop="status" width="80">
<template slot-scope="scope">
<el-tag v-if="scope.row.status === '0'" type="success" size="small">正常</el-tag>
<el-tag v-else type="danger" size="small">停用</el-tag>
</template>
</el-table-column>
</el-table>
<pagination
v-show="userSelectTotal > 0"
:total="userSelectTotal"
:page.sync="userQueryParams.pageNum"
:limit.sync="userQueryParams.pageSize"
@pagination="getUserSelectList"
/>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="handleConfirmUserSelect"> </el-button>
<el-button @click="showUserSelectDialog = false"> </el-button>
</div>
</el-dialog>
</div> </div>
</template> </template>
@ -179,6 +297,7 @@ import { listScale } from "@/api/psychology/scale";
import { allocatedUserList } from "@/api/system/role"; import { allocatedUserList } from "@/api/system/role";
import { listRole } from "@/api/system/role"; import { listRole } from "@/api/system/role";
import { getUser } from "@/api/system/user"; import { getUser } from "@/api/system/user";
import { listProfile } from "@/api/psychology/profile";
export default { export default {
name: "PsyScalePermission", name: "PsyScalePermission",
@ -200,6 +319,8 @@ export default {
permissionList: [], permissionList: [],
// //
scaleList: [], scaleList: [],
//
scaleSearchKeyword: "",
// //
userOptions: [], userOptions: [],
// //
@ -210,6 +331,26 @@ export default {
title: "", title: "",
// //
open: false, open: false,
//
showUserSelectDialog: false,
//
userSelectList: [],
userSelectTotal: 0,
userSelectLoading: false,
selectedUserIds: [],
cachedFilteredUsers: [],
updatingSelection: false,
//
prisonAreaOptions: [],
//
userQueryParams: {
pageNum: 1,
pageSize: 8,
infoNumber: undefined,
userName: undefined,
prisonArea: undefined,
status: '0'
},
// //
queryParams: { queryParams: {
pageNum: 1, pageNum: 1,
@ -231,12 +372,71 @@ export default {
} }
}; };
}, },
computed: {
//
filteredScaleList() {
if (!this.scaleSearchKeyword) {
return this.scaleList;
}
const keyword = this.scaleSearchKeyword.toLowerCase();
return this.scaleList.filter(scale => {
const scaleName = (scale.scaleName || '').toLowerCase();
const scaleCode = (scale.scaleCode || '').toLowerCase();
return scaleName.includes(keyword) || scaleCode.includes(keyword);
});
},
//
selectedUserDisplay() {
const userIds = this.form.userIds || [];
if (userIds.length === 0) {
return "";
} else if (userIds.length === 1) {
const user = this.userOptions.find(u => u.userId === userIds[0]);
return user ? (user.nickName ? `${user.nickName}${user.userName}` : user.userName) : "";
} else {
return `已选择 ${userIds.length} 个用户`;
}
}
},
watch: {
//
showUserSelectDialog(val) {
if (val) {
//
this.resetUserQuery();
this.selectedUserIds = [...(this.form.userIds || [])];
this.getUserSelectList().then(() => {
//
this.$nextTick(() => {
if (this.form.userIds && this.form.userIds.length > 0 && this.$refs.userTable) {
this.form.userIds.forEach(userId => {
const row = this.userSelectList.find(u => u.userId === userId);
if (row) {
this.$refs.userTable.toggleRowSelection(row, true);
}
});
}
});
});
} else {
this.cachedFilteredUsers = [];
}
}
},
created() { created() {
this.getList(); this.getList();
this.loadScales(); this.loadScales();
this.loadUsers(); this.loadUsers();
this.loadPrisonAreaOptions();
}, },
methods: { methods: {
formatScaleLabel(scale) {
if (!scale) {
return ""
}
const prefix = scale.sourceType === 'questionnaire' ? '[问卷]' : '[量表]'
return `${prefix} ${scale.scaleName || ''}`
},
/** 查询权限列表 */ /** 查询权限列表 */
getList() { getList() {
this.loading = true; this.loading = true;
@ -324,11 +524,16 @@ export default {
}, },
/** 加载量表列表(显示所有量表,不包含问卷) */ /** 加载量表列表(显示所有量表,不包含问卷) */
loadScales() { loadScales() {
// // /
listScale({ includeQuestionnaire: false, pageNum: 1, pageSize: 1000 }).then(response => { listScale({ includeQuestionnaire: true, pageNum: 1, pageSize: 1000 }).then(response => {
// this.scaleList = (response.rows || []).filter(scale => {
this.scaleList = (response.rows || []) if (!scale) return false
.filter(scale => !scale.sourceType || scale.sourceType === 'scale'); //
if (scale.sourceType === 'questionnaire') {
return true
}
return true
})
}); });
}, },
/** 加载用户列表(只加载学员角色的用户)- 改为远程搜索模式,不预加载 */ /** 加载用户列表(只加载学员角色的用户)- 改为远程搜索模式,不预加载 */
@ -345,6 +550,219 @@ export default {
console.error("获取学员角色ID失败:", error); console.error("获取学员角色ID失败:", error);
}); });
}, },
/** 加载监区下拉选项 */
loadPrisonAreaOptions() {
listProfile({ pageNum: 1, pageSize: 10000 }).then(response => {
const rows = response.rows || [];
const areaSet = new Set();
rows.forEach(profile => {
if (profile && profile.prisonArea) {
areaSet.add(profile.prisonArea);
}
});
this.prisonAreaOptions = Array.from(areaSet);
}).catch(error => {
console.error("加载监区列表失败:", error);
});
},
/** 量表行点击 */
handleScaleRowClick(row) {
this.form.scaleId = row.scaleId;
},
/** 量表选择改变 */
handleScaleChange(scale) {
this.form.scaleId = scale.scaleId;
},
transformProfileToSelectableUser(profile) {
return {
userId: profile.userId,
infoNumber: profile.infoNumber,
userName: profile.userName,
userAccount: profile.infoNumber,
prisonArea: profile.prisonArea || profile.deptName || "-",
status: profile.status || '0'
};
},
/** 获取用户选择列表 */
getUserSelectList() {
this.userSelectLoading = true;
// 使
const query = {
pageNum: this.userQueryParams.pageNum,
pageSize: this.userQueryParams.pageSize,
infoNumber: this.userQueryParams.infoNumber,
userName: this.userQueryParams.userName,
prisonArea: this.userQueryParams.prisonArea,
status: this.userQueryParams.status
};
return listProfile(query).then(response => {
this.userSelectList = (response.rows || []).map(profile => this.transformProfileToSelectableUser(profile));
this.userSelectTotal = response.total || 0;
this.userSelectLoading = false;
this.$nextTick(() => {
this.restoreUserSelection();
});
return response;
}).catch(() => {
this.userSelectLoading = false;
return Promise.reject();
});
},
/** 用户查询 */
handleUserQuery() {
this.userQueryParams.pageNum = 1;
this.cachedFilteredUsers = [];
this.getUserSelectList();
},
/** 重置用户查询 */
resetUserQuery() {
this.resetForm("userQueryForm");
this.userQueryParams = {
pageNum: 1,
pageSize: 8,
infoNumber: undefined,
userName: undefined,
prisonArea: undefined,
status: '0'
};
this.handleUserQuery();
},
/** 用户选择改变 */
handleUserSelectionChange(selection) {
if (this.updatingSelection) {
return;
}
const currentPageIds = this.userSelectList.map(item => item.userId);
//
this.selectedUserIds = this.selectedUserIds.filter(id => !currentPageIds.includes(id));
selection.forEach(item => {
if (!this.selectedUserIds.includes(item.userId)) {
this.selectedUserIds.push(item.userId);
}
});
},
async handleUserSelectAll(selection) {
if (selection.length === 0) {
//
const currentPageIds = this.userSelectList.map(item => item.userId);
this.selectedUserIds = this.selectedUserIds.filter(id => !currentPageIds.includes(id));
if (currentPageIds.length === 0) {
this.selectedUserIds = [];
}
this.cachedFilteredUsers = [];
return;
}
if (selection.length === this.userSelectList.length && this.userSelectList.length > 0) {
await this.selectAllUsersUnderCurrentFilter();
}
},
async fetchAllUsersUnderCurrentFilter() {
const pageSize = 500;
let pageNum = 1;
let total = 0;
const allUsers = [];
const baseParams = {
infoNumber: this.userQueryParams.infoNumber,
userName: this.userQueryParams.userName,
prisonArea: this.userQueryParams.prisonArea,
status: this.userQueryParams.status
};
while (true) {
const response = await listProfile({
...baseParams,
pageNum,
pageSize
});
const rows = response.rows || [];
if (response.total !== undefined && response.total !== null) {
total = response.total;
} else if (total === 0 && rows.length > 0) {
total = rows.length;
}
allUsers.push(...rows.map(profile => this.transformProfileToSelectableUser(profile)));
if (rows.length < pageSize || (total > 0 && allUsers.length >= total)) {
break;
}
pageNum += 1;
}
return allUsers;
},
async selectAllUsersUnderCurrentFilter() {
try {
this.userSelectLoading = true;
const users = await this.fetchAllUsersUnderCurrentFilter();
if (!users.length) {
this.selectedUserIds = [];
this.cachedFilteredUsers = [];
this.$message.warning("当前筛选下没有可选用户");
return;
}
this.cachedFilteredUsers = users;
const allIds = users
.map(user => user.userId)
.filter(userId => userId !== undefined && userId !== null);
this.selectedUserIds = Array.from(new Set(allIds));
this.$nextTick(() => {
this.restoreUserSelection();
});
this.$message.success(`已选择当前筛选下的 ${this.selectedUserIds.length} 名用户`);
} catch (error) {
console.error("批量选择用户失败:", error);
this.$message.error("批量选择用户失败,请稍后重试");
} finally {
this.userSelectLoading = false;
}
},
/** 确认用户选择 */
handleConfirmUserSelect() {
// form.userIds
this.form.userIds = [...this.selectedUserIds];
// userOptions
this.selectedUserIds.forEach(userId => {
const user =
this.userSelectList.find(u => u.userId === userId) ||
this.cachedFilteredUsers.find(u => u.userId === userId);
if (user && !this.userOptions.find(u => u.userId === userId)) {
this.userOptions.push({
userId: user.userId,
userName: user.userAccount || user.userName,
nickName: user.userName
});
}
});
this.showUserSelectDialog = false;
this.cachedFilteredUsers = [];
},
restoreUserSelection() {
if (!this.$refs.userTable) {
return;
}
this.updatingSelection = true;
this.$refs.userTable.clearSelection();
this.userSelectList.forEach(row => {
if (this.selectedUserIds.includes(row.userId)) {
this.$refs.userTable.toggleRowSelection(row, true);
}
});
this.$nextTick(() => {
this.updatingSelection = false;
});
},
/** 全选所有用户按钮点击事件 */
handleSelectAllUsers() {
this.selectAllUsersUnderCurrentFilter();
},
/** 取消全选按钮点击事件 */
handleDeselectAllUsers() {
this.selectedUserIds = [];
this.cachedFilteredUsers = [];
this.$nextTick(() => {
if (this.$refs.userTable) {
this.$refs.userTable.clearSelection();
}
});
this.$message.success("已取消所有选择");
},
/** 搜索用户(远程搜索) */ /** 搜索用户(远程搜索) */
searchUsers(keyword) { searchUsers(keyword) {
if (!this.studentRoleId) { if (!this.studentRoleId) {
@ -545,6 +963,9 @@ export default {
_permissionIds: undefined, // ID _permissionIds: undefined, // ID
_groupKey: undefined // key _groupKey: undefined // key
}; };
this.scaleSearchKeyword = "";
this.selectedUserIds = [];
this.cachedFilteredUsers = [];
this.resetForm("form"); this.resetForm("form");
}, },
/** 搜索按钮操作 */ /** 搜索按钮操作 */
@ -574,6 +995,7 @@ export default {
/** 新增按钮操作 */ /** 新增按钮操作 */
handleAdd() { handleAdd() {
this.reset(); this.reset();
this.scaleSearchKeyword = "";
// userOptions // userOptions
if (this.userOptions.length === 0) { if (this.userOptions.length === 0) {
this.searchUsers(""); this.searchUsers("");
@ -584,6 +1006,7 @@ export default {
/** 修改按钮操作 */ /** 修改按钮操作 */
handleUpdate(row) { handleUpdate(row) {
this.reset(); this.reset();
this.scaleSearchKeyword = "";
// ID // ID
const permissionIds = row.permissionIds || (row.permissionId ? [row.permissionId] : []); const permissionIds = row.permissionIds || (row.permissionId ? [row.permissionId] : []);

View File

@ -17,14 +17,6 @@
@keyup.enter.native="handleQuery" @keyup.enter.native="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="监狱" prop="prison">
<el-input
v-model="queryParams.prison"
placeholder="请输入监狱名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="监区" prop="prisonArea"> <el-form-item label="监区" prop="prisonArea">
<el-input <el-input
v-model="queryParams.prisonArea" v-model="queryParams.prisonArea"
@ -61,14 +53,6 @@
<el-option label="其他" value="其他" /> <el-option label="其他" value="其他" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="罪名" prop="crimeName">
<el-input
v-model="queryParams.crimeName"
placeholder="请输入罪名"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
</el-form-item> </el-form-item>
@ -126,6 +110,11 @@
<span v-else>{{ scope.row.gender || '-' }}</span> <span v-else>{{ scope.row.gender || '-' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="出生日期" align="center" prop="birthday" width="120">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.birthday, '{y}-{m}-{d}') || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="民族" align="center" prop="nation" width="80"> <el-table-column label="民族" align="center" prop="nation" width="80">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ scope.row.nation || '-' }}</span> <span>{{ scope.row.nation || '-' }}</span>
@ -136,11 +125,6 @@
<span>{{ scope.row.educationLevel || '-' }}</span> <span>{{ scope.row.educationLevel || '-' }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="出生日期" align="center" prop="birthday" width="120">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.birthday, '{y}-{m}-{d}') || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="罪名" align="center" prop="crimeName" width="120"> <el-table-column label="罪名" align="center" prop="crimeName" width="120">
<template slot-scope="scope"> <template slot-scope="scope">
<span>{{ scope.row.crimeName || '-' }}</span> <span>{{ scope.row.crimeName || '-' }}</span>
@ -258,12 +242,23 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="民族" prop="nation"> <el-form-item label="出生日期" prop="birthday">
<el-input v-model="form.nation" placeholder="请输入民族" /> <el-date-picker
v-model="form.birthday"
type="date"
placeholder="选择出生日期"
value-format="yyyy-MM-dd"
style="width: 100%"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row> <el-row>
<el-col :span="12">
<el-form-item label="民族" prop="nation">
<el-input v-model="form.nation" placeholder="请输入民族" />
</el-form-item>
</el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="文化程度" prop="educationLevel"> <el-form-item label="文化程度" prop="educationLevel">
<el-select v-model="form.educationLevel" placeholder="请选择文化程度"> <el-select v-model="form.educationLevel" placeholder="请选择文化程度">
@ -279,20 +274,14 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12">
<el-form-item label="出生日期" prop="birthday">
<el-date-picker
v-model="form.birthday"
type="date"
placeholder="选择出生日期"
value-format="yyyy-MM-dd"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row> </el-row>
<el-row> <el-row>
<el-col :span="24"> <el-col :span="12">
<el-form-item label="监狱" prop="prison" v-if="false">
<el-input v-model="form.prison" placeholder="请输入监狱名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="罪名" prop="crimeName"> <el-form-item label="罪名" prop="crimeName">
<el-input v-model="form.crimeName" placeholder="请输入罪名" /> <el-input v-model="form.crimeName" placeholder="请输入罪名" />
</el-form-item> </el-form-item>
@ -434,28 +423,42 @@
<!-- 用户档案导入对话框 --> <!-- 用户档案导入对话框 -->
<el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body> <el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
<el-upload ref="upload" :limit="1" accept=".xlsx, .xls" :headers="upload.headers" :action="upload.url + '?updateSupport=' + upload.updateSupport" :disabled="upload.isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :on-error="handleFileError" :auto-upload="false" drag> <el-upload
ref="upload"
:limit="1"
accept=".xlsx, .xls"
:headers="upload.headers"
:action="upload.url + '?updateSupport=' + upload.updateSupport"
:disabled="upload.isUploading"
:on-progress="handleFileUploadProgress"
:on-success="handleFileSuccess"
:on-error="handleFileError"
:auto-upload="false"
drag>
<i class="el-icon-upload"></i> <i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div> <div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<div class="el-upload__tip text-center" slot="tip"> <div class="el-upload__tip text-center" slot="tip">
<div class="el-upload__tip" slot="tip">
<el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户档案数据
</div>
<span>仅允许导入xlsxlsx格式文件</span> <span>仅允许导入xlsxlsx格式文件</span>
<el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">下载模板</el-link> <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">下载模板</el-link>
</div> </div>
<div v-if="upload.showProgress" class="upload-progress">
<el-progress :percentage="upload.progress"></el-progress>
<div class="upload-progress__text">{{ upload.statusText }}</div>
</div>
</el-upload> </el-upload>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitFileForm"> </el-button> <el-button type="primary" :loading="upload.isUploading" @click="submitFileForm"> </el-button>
<el-button @click="upload.open = false"> </el-button> <el-button :disabled="upload.isUploading" @click="upload.open = false"> </el-button>
</div> </div>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script> <script>
import { listStudentProfile, getProfile, getProfileByUserId, delProfile, addProfile, updateProfile, getUserInfo, addUserInProfile, getUserInfoById, updateUserInProfile, delUserInProfile } from "@/api/psychology/profile" import { listStudentProfile, getProfile, getProfileByUserId, delProfile, addProfile, updateProfile, getUserInfo, addUserInProfile, getUserInfoById, updateUserInProfile, delUserInProfile, getProfileImportProgress } from "@/api/psychology/profile"
import { deptTreeSelect } from "@/api/system/user" import { deptTreeSelect } from "@/api/system/user"
import { allocatedUserList } from "@/api/system/role"
import { listRole } from "@/api/system/role"
import { getToken } from "@/utils/auth" import { getToken } from "@/utils/auth"
import Treeselect from "@riophae/vue-treeselect" import Treeselect from "@riophae/vue-treeselect"
import "@riophae/vue-treeselect/dist/vue-treeselect.css" import "@riophae/vue-treeselect/dist/vue-treeselect.css"
@ -496,6 +499,11 @@ export default {
postOptions: [], postOptions: [],
// //
roleOptions: [], roleOptions: [],
// ID
studentRoleId: undefined,
// /
filterPrisonOptions: [],
filterPrisonAreaOptions: [],
// //
initPassword: undefined, initPassword: undefined,
// //
@ -513,7 +521,9 @@ export default {
educationLevel: undefined, educationLevel: undefined,
crimeName: undefined, crimeName: undefined,
userId: undefined, userId: undefined,
deptId: undefined deptId: undefined,
status: undefined,
idCard: undefined
}, },
// //
form: {}, form: {},
@ -523,11 +533,12 @@ export default {
{ required: true, message: "档案类型不能为空", trigger: "change" } { required: true, message: "档案类型不能为空", trigger: "change" }
], ],
infoNumber: [ infoNumber: [
{ required: true, message: "信息编号不能为空", trigger: "blur" },
{ pattern: /^\d+$/, message: "信息编号只能输入数字", trigger: "blur" } { pattern: /^\d+$/, message: "信息编号只能输入数字", trigger: "blur" }
], ],
userName: [ userName: [
{ required: true, message: "罪犯姓名不能为空", trigger: "blur" }, { required: true, message: "罪犯姓名不能为空", trigger: "blur" },
{ pattern: /^[\u4e00-\u9fa5]+$/, message: "姓名只能输入汉字", trigger: "blur" } { pattern: /^[\u4e00-\u9fa5\d]+$/, message: "姓名只能输入汉字和数字", trigger: "blur" }
], ],
prisonArea: [ prisonArea: [
{ required: true, message: "监区不能为空", trigger: "blur" } { required: true, message: "监区不能为空", trigger: "blur" }
@ -574,16 +585,27 @@ export default {
// //
headers: { Authorization: "Bearer " + getToken() }, headers: { Authorization: "Bearer " + getToken() },
// //
url: process.env.VUE_APP_BASE_API + "/psychology/profile/importData" url: process.env.VUE_APP_BASE_API + "/psychology/profile/importData",
} //
showProgress: false,
progress: 0,
statusText: "",
stage: "idle"
},
importProgressTimer: null
} }
}, },
beforeDestroy() {
this.stopImportProgressPolling()
},
created() { created() {
this.getList() this.getList()
this.getDeptTree() this.getDeptTree()
this.getConfigKey("sys.user.initPassword").then(response => { this.getConfigKey("sys.user.initPassword").then(response => {
this.initPassword = response.msg this.initPassword = response.msg
}) })
// ID
this.initStudentRoleId()
}, },
methods: { methods: {
/** 查询学员档案列表(后端分页) */ /** 查询学员档案列表(后端分页) */
@ -619,6 +641,38 @@ export default {
this.loading = false this.loading = false
}) })
}, },
/** 过滤学员用户 */
filterStudentUsers(rows) {
return new Promise((resolve, reject) => {
if (!this.studentRoleId || rows.length === 0) {
resolve(rows)
return
}
// ID
allocatedUserList({
roleId: this.studentRoleId,
status: '0',
pageNum: 1,
pageSize: 10000
}).then(response => {
const studentUserIds = new Set()
const studentUsers = response.rows || []
studentUsers.forEach(user => {
if (user.userId) {
studentUserIds.add(user.userId)
}
})
//
const filteredRows = rows.filter(row => {
return row.userId && studentUserIds.has(row.userId)
})
resolve(filteredRows)
}).catch(error => {
console.error("获取学员用户列表失败:", error)
reject(error)
})
})
},
// //
cancel() { cancel() {
this.open = false this.open = false
@ -635,9 +689,9 @@ export default {
prison: undefined, prison: undefined,
prisonArea: undefined, prisonArea: undefined,
gender: undefined, gender: undefined,
birthday: undefined,
nation: undefined, nation: undefined,
educationLevel: undefined, educationLevel: undefined,
birthday: undefined,
crimeName: undefined, crimeName: undefined,
sentenceTerm: undefined, sentenceTerm: undefined,
sentenceStartDate: undefined, sentenceStartDate: undefined,
@ -654,8 +708,8 @@ export default {
}, },
// //
handleUserNameInput(value) { handleUserNameInput(value) {
// //
this.form.userName = value.replace(/[^\u4e00-\u9fa5]/g, '') this.form.userName = value.replace(/[^\u4e00-\u9fa5\d]/g, '')
}, },
/** 搜索按钮操作 */ /** 搜索按钮操作 */
handleQuery() { handleQuery() {
@ -874,7 +928,6 @@ export default {
gender: (row && row.gender) || undefined, gender: (row && row.gender) || undefined,
nation: (row && row.nation) || undefined, nation: (row && row.nation) || undefined,
educationLevel: (row && row.educationLevel) || undefined, educationLevel: (row && row.educationLevel) || undefined,
birthday: (row && row.birthday) || undefined,
crimeName: (row && row.crimeName) || undefined, crimeName: (row && row.crimeName) || undefined,
sentenceTerm: (row && row.sentenceTerm) || undefined, sentenceTerm: (row && row.sentenceTerm) || undefined,
sentenceStartDate: undefined, sentenceStartDate: undefined,
@ -896,6 +949,7 @@ export default {
prison: (row && row.prison) || undefined, prison: (row && row.prison) || undefined,
prisonArea: (row && row.prisonArea) || undefined, prisonArea: (row && row.prisonArea) || undefined,
gender: (row && row.gender) || undefined, gender: (row && row.gender) || undefined,
birthday: (row && row.birthday) || undefined,
nation: (row && row.nation) || undefined, nation: (row && row.nation) || undefined,
educationLevel: (row && row.educationLevel) || undefined, educationLevel: (row && row.educationLevel) || undefined,
crimeName: (row && row.crimeName) || undefined, crimeName: (row && row.crimeName) || undefined,
@ -948,7 +1002,12 @@ export default {
this.$modal.msgError("请选择要删除的记录") this.$modal.msgError("请选择要删除的记录")
return return
} }
//
this.checkStudentUsers(targets).then(isAllStudents => {
if (!isAllStudents) {
this.$modal.msgError("只能删除学员用户,系统管理员等非学员用户不能删除")
return
}
const profileIdSet = new Set() const profileIdSet = new Set()
const userIdSet = new Set() const userIdSet = new Set()
const userIdsWithProfile = new Set() // ID const userIdsWithProfile = new Set() // ID
@ -1007,6 +1066,69 @@ export default {
const errorMsg = error.msg || error.message || "删除失败" const errorMsg = error.msg || error.message || "删除失败"
this.$modal.msgError(errorMsg) this.$modal.msgError(errorMsg)
}) })
}).catch(error => {
console.error("检查学员用户失败:", error)
this.$modal.msgError("检查用户角色失败,无法删除")
})
},
/** 初始化学员角色ID */
initStudentRoleId() {
listRole({}).then(response => {
const roles = response.rows || response.data || []
if (!Array.isArray(roles) || roles.length === 0) {
return
}
let candidate = roles.find(r => r && r.roleKey && r.roleKey.toLowerCase() === 'student')
if (!candidate) {
candidate = roles.find(r => r && r.roleName && r.roleName.indexOf('学员') !== -1)
}
if (!candidate) {
candidate = roles.find(r => r && r.roleId === 101)
}
if (candidate && candidate.roleId) {
this.studentRoleId = candidate.roleId
}
}).catch(() => {
//
})
},
/** 检查用户是否都是学员用户 */
checkStudentUsers(targets) {
return new Promise((resolve, reject) => {
if (!targets || targets.length === 0) {
resolve(false)
return
}
// ID
if (!this.studentRoleId) {
this.$modal.msgError('未正确配置学员角色,暂不允许删除用户,请联系管理员')
resolve(false)
return
}
// ID
allocatedUserList({
roleId: this.studentRoleId,
status: '0',
pageNum: 1,
pageSize: 10000
}).then(response => {
const studentUserIds = new Set()
const studentUsers = response.rows || []
studentUsers.forEach(user => {
if (user.userId) {
studentUserIds.add(user.userId)
}
})
//
const allAreStudents = targets.every(item => {
return item.userId && studentUserIds.has(item.userId)
})
resolve(allAreStudents)
}).catch(error => {
console.error("获取学员用户列表失败:", error)
reject(error)
})
})
}, },
/** 导出按钮操作 */ /** 导出按钮操作 */
handleExport() { handleExport() {
@ -1052,7 +1174,78 @@ export default {
this.$modal.msgError('请选择后缀为 "xls"或"xlsx"的文件。') this.$modal.msgError('请选择后缀为 "xls"或"xlsx"的文件。')
return return
} }
this.upload.showProgress = true
this.upload.progress = 0
this.upload.statusText = "准备上传..."
this.upload.isUploading = true
this.upload.stage = "uploading"
this.$refs.upload.submit() this.$refs.upload.submit()
this.startImportProgressPolling()
},
normalizeUploadResponse(payload) {
if (!payload) {
return {}
}
if (typeof payload === "string") {
try {
return JSON.parse(payload)
} catch (error) {
return { msg: payload }
}
}
return payload
},
resetUploadProgress() {
this.upload.isUploading = false
this.upload.showProgress = false
this.upload.progress = 0
this.upload.statusText = ""
this.upload.stage = "idle"
this.stopImportProgressPolling()
},
startImportProgressPolling() {
if (this.importProgressTimer) {
clearInterval(this.importProgressTimer)
}
this.importProgressTimer = setInterval(() => {
this.fetchImportProgress()
}, 1000)
},
stopImportProgressPolling() {
if (this.importProgressTimer) {
clearInterval(this.importProgressTimer)
this.importProgressTimer = null
}
},
fetchImportProgress() {
getProfileImportProgress().then(response => {
const progress = response.data || response
if (!progress) {
return
}
this.upload.showProgress = true
this.upload.stage = "processing"
const percent = progress.total > 0 ? Math.round((progress.processed / progress.total) * 100) : 0
this.upload.progress = Math.min(Math.max(percent, 0), 100)
this.upload.statusText = this.formatProgressStatus(progress)
if (["success", "failed"].includes(progress.status)) {
this.stopImportProgressPolling()
}
}).catch(() => {
//
})
},
formatProgressStatus(progress) {
if (!progress) {
return "正在处理..."
}
if (progress.status === "success") {
return `导入成功,共 ${progress.success}`
}
if (progress.status === "failed") {
return `导入失败,成功 ${progress.success} 条,失败 ${progress.failure}`
}
return `正在处理:已完成 ${progress.processed}/${progress.total}`
}, },
/** 状态变更 */ /** 状态变更 */
handleStatusChange(row) { handleStatusChange(row) {
@ -1187,5 +1380,15 @@ export default {
.app-container { .app-container {
padding: 20px; padding: 20px;
} }
.upload-progress {
margin-top: 16px;
padding: 0 10px 10px;
}
.upload-progress__text {
margin-top: 8px;
font-size: 12px;
color: #606266;
text-align: center;
}
</style> </style>

View File

@ -111,7 +111,7 @@
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span> <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="360"> <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="420">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button <el-button
size="mini" size="mini"
@ -334,6 +334,13 @@ export default {
}, },
created() { created() {
this.getList() this.getList()
// action: 'edit' questionnaireId
const route = this.$route
if (route.query.action === 'edit' && route.query.questionnaireId) {
this.$nextTick(() => {
this.handleUpdateFromRoute(route.query.questionnaireId)
})
}
}, },
methods: { methods: {
/** 查询问卷列表 */ /** 查询问卷列表 */
@ -395,10 +402,31 @@ export default {
handleUpdate(row) { handleUpdate(row) {
this.reset() this.reset()
const questionnaireId = row.questionnaireId || this.ids[0] const questionnaireId = row.questionnaireId || this.ids[0]
this.handleUpdateFromRoute(questionnaireId)
},
/** 从路由参数或传入的ID打开编辑对话框 */
handleUpdateFromRoute(questionnaireId) {
if (!questionnaireId) {
return
}
this.reset()
getQuestionnaire(questionnaireId).then(response => { getQuestionnaire(questionnaireId).then(response => {
this.form = response.data this.form = response.data
this.open = true this.open = true
this.title = "修改问卷" this.title = "修改问卷"
//
if (this.$route.query.action === 'edit') {
const newQuery = { ...this.$route.query }
delete newQuery.action
delete newQuery.questionnaireId
this.$router.replace({
path: this.$route.path,
query: Object.keys(newQuery).length > 0 ? newQuery : {}
})
}
}).catch(error => {
console.error("获取问卷信息失败:", error)
this.$modal.msgError("获取问卷信息失败")
}) })
}, },
/** 提交按钮 */ /** 提交按钮 */

View File

@ -3,33 +3,30 @@
<el-card class="user-selector" shadow="never"> <el-card class="user-selector" shadow="never">
<el-form :inline="true" size="small"> <el-form :inline="true" size="small">
<el-form-item label="选择用户"> <el-form-item label="选择用户">
<el-select <el-autocomplete
v-model="selectedUserId" v-model="userSearchKeyword"
style="width: 280px" style="width: 320px"
clearable clearable
filterable :fetch-suggestions="searchUsers"
remote placeholder="输入姓名或信息编号搜索"
reserve-keyword value-key="value"
:remote-method="searchUsers" :trigger-on-focus="false"
:loading="userOptionsLoading" :debounce="400"
placeholder="输入姓名或账号搜索" @select="handleUserSelect"
@change="handleUserChange"
>
<el-option
v-for="item in userOptions"
:key="item.userId"
:label="buildUserLabel(item)"
:value="item.userId"
> >
<template slot-scope="{ item }">
<div class="user-option"> <div class="user-option">
<span class="name">{{ item.nickName || item.userName }}</span> <span class="name">{{ item.nickName || item.userName }}</span>
<span class="dept">{{ item.deptName || '未分配单位' }}</span> <span class="dept">{{ item.infoNumber ? `编号:${item.infoNumber}` : '' }}</span>
</div> </div>
</el-option> </template>
</el-select> </el-autocomplete>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" icon="el-icon-search" :disabled="!selectedUserId" @click="loadUserData"> <el-button type="primary" icon="el-icon-search" @click="handleManualSearch">
搜索
</el-button>
<el-button type="success" icon="el-icon-refresh" :disabled="!selectedUserId" @click="loadUserData">
载入 载入
</el-button> </el-button>
<el-button icon="el-icon-refresh" @click="resetSelection">重置</el-button> <el-button icon="el-icon-refresh" @click="resetSelection">重置</el-button>
@ -53,7 +50,7 @@
<el-card class="scale-list" shadow="never" v-loading="loading"> <el-card class="scale-list" shadow="never" v-loading="loading">
<div slot="header"> <div slot="header">
<span>量表列表请勾选需要分析的量表</span> <span>量表/问卷列表请勾选需要分析的量表/问卷</span>
<div class="header-actions"> <div class="header-actions">
<el-button <el-button
type="primary" type="primary"
@ -76,7 +73,17 @@
empty-text="暂无可用的测评报告" empty-text="暂无可用的测评报告"
> >
<el-table-column type="selection" width="55" /> <el-table-column type="selection" width="55" />
<el-table-column prop="scaleName" label="量表名称" min-width="200" /> <el-table-column prop="scaleName" label="量表/问卷名称" min-width="220">
<template slot-scope="scope">
<el-tag
v-if="scope.row.sourceType === 'questionnaire'"
size="mini"
type="warning"
style="margin-right: 6px"
>问卷</el-tag>
<span>{{ scope.row.scaleName || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="reportTitle" label="报告标题" min-width="240" :show-overflow-tooltip="true" /> <el-table-column prop="reportTitle" label="报告标题" min-width="240" :show-overflow-tooltip="true" />
<el-table-column label="测评时间" width="200" align="center"> <el-table-column label="测评时间" width="200" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
@ -107,6 +114,12 @@
<div v-else class="empty-report">报告生成中请稍候...</div> <div v-else class="empty-report">报告生成中请稍候...</div>
</div> </div>
<div slot="footer"> <div slot="footer">
<el-button
type="success"
icon="el-icon-printer"
:disabled="!comprehensiveReport"
@click="printReport"
>打印报告</el-button>
<el-button <el-button
type="primary" type="primary"
icon="el-icon-download" icon="el-icon-download"
@ -120,8 +133,8 @@
<script> <script>
import { getUserAssessmentSummary, getStudentOptions } from '@/api/psychology/assessment' import { getUserAssessmentSummary, getStudentOptions } from '@/api/psychology/assessment'
import { getProfileByUserId } from '@/api/psychology/profile' import { getProfileByUserId, listProfile } from '@/api/psychology/profile'
import { getReport } from '@/api/psychology/report' import { getReport, listReport } from '@/api/psychology/report'
import { parseTime } from '@/utils/ruoyi' import { parseTime } from '@/utils/ruoyi'
import axios from 'axios' import axios from 'axios'
@ -130,8 +143,9 @@ export default {
data() { data() {
return { return {
selectedUserId: undefined, selectedUserId: undefined,
userOptions: [], userSearchKeyword: '',
userOptionsLoading: false, userSearchLoading: false,
cachedUserOptions: [],
userProfile: null, userProfile: null,
userSummary: null, userSummary: null,
reportOptions: [], reportOptions: [],
@ -140,22 +154,57 @@ export default {
generating: false, generating: false,
reportDialogVisible: false, reportDialogVisible: false,
comprehensiveReport: '', comprehensiveReport: '',
// ========== ==========
OLLAMA_URL: 'http://192.168.0.106:11434/api/generate', OLLAMA_URL: 'http://192.168.0.106:11434/api/generate',
MODEL: 'deepseek-r1:32b' MODEL: 'deepseek-r1:32b'
} }
}, },
created() { created() {
this.searchUsers('')
}, },
methods: { methods: {
searchUsers(query) { searchUsers(query, cb) {
this.userOptionsLoading = true this.fetchUserOptions(query)
getStudentOptions({ keyword: query, limit: 20 }) .then((list) => cb(list))
.then((res) => { .catch(() => cb([]))
this.userOptions = res.data || [] },
fetchUserOptions(keyword) {
const trimmed = (keyword || '').trim()
if (!trimmed) {
return Promise.resolve([])
}
this.userSearchLoading = true
const studentPromise = getStudentOptions({ keyword: trimmed, limit: 20 })
.then((res) => this.normalizeStudentOptions(res.data || []))
.catch(() => [])
let profilePromise = Promise.resolve([])
//
if (/^\d+$/.test(trimmed)) {
profilePromise = listProfile({
infoNumber: trimmed,
pageNum: 1,
pageSize: 20
})
.then((res) => this.normalizeProfileOptions(res.rows || []))
.catch(() => [])
} else {
//
profilePromise = listProfile({
userName: trimmed,
pageNum: 1,
pageSize: 20
})
.then((res) => this.normalizeProfileOptions(res.rows || []))
.catch(() => [])
}
return Promise.all([studentPromise, profilePromise])
.then(([studentList, profileList]) => {
// 使infoNumber
const merged = this.mergeUserOptions([...profileList, ...studentList])
this.cachedUserOptions = merged
return merged
}) })
.finally(() => { .finally(() => {
this.userOptionsLoading = false this.userSearchLoading = false
}) })
}, },
buildUserLabel(option) { buildUserLabel(option) {
@ -163,13 +212,112 @@ export default {
return '' return ''
} }
const name = option.nickName || option.userName || '' const name = option.nickName || option.userName || ''
const info = option.infoNumber ? `(编号:${option.infoNumber}` : ''
const dept = option.deptName ? ` - ${option.deptName}` : '' const dept = option.deptName ? ` - ${option.deptName}` : ''
return name + dept return `${name}${info}${dept}`
}, },
handleUserChange() { handleUserSelect(option) {
if (!this.selectedUserId) { if (!option || !option.userId) {
this.resetSelection() return
} }
this.selectedUserId = option.userId
this.userSearchKeyword = this.buildUserLabel(option)
},
handleManualSearch() {
const keyword = (this.userSearchKeyword || '').trim()
if (!keyword) {
this.$message.warning('请输入姓名或信息编号')
return
}
//
let searchKeyword = keyword
//
const numberMatch = keyword.match(/编号[:]\s*(\d+)/)
if (numberMatch) {
searchKeyword = numberMatch[1]
} else {
//
const nameMatch = keyword.match(/^([^(]+)/)
if (nameMatch) {
searchKeyword = nameMatch[1].trim()
}
}
this.fetchUserOptions(searchKeyword).then((list) => {
if (!list.length) {
//
//
return
}
if (list.length === 1) {
//
this.handleUserSelect(list[0])
} else {
//
const exactMatch = list.find(opt => {
const label = this.buildUserLabel(opt)
return label === keyword
})
if (exactMatch) {
//
this.handleUserSelect(exactMatch)
} else {
//
this.$message.info('找到多条记录,请从下拉列表选择具体用户')
}
}
}).catch(() => {
//
})
},
normalizeStudentOptions(list) {
return list.map((item) => ({
userId: item.userId,
userName: item.userName,
nickName: item.nickName,
infoNumber: item.infoNumber,
deptName: item.deptName,
value: this.buildUserLabel(item)
}))
},
normalizeProfileOptions(rows) {
return rows
.filter((profile) => profile && profile.userId)
.map((profile) => ({
userId: profile.userId,
userName: profile.userName || profile.nickName,
nickName: profile.userName || profile.nickName,
infoNumber: profile.infoNumber,
deptName: profile.prisonArea || profile.deptName,
value: this.buildUserLabel({
userName: profile.userName || profile.nickName,
nickName: profile.userName || profile.nickName,
infoNumber: profile.infoNumber
})
}))
},
mergeUserOptions(list) {
const map = new Map()
list.forEach((item) => {
if (!item || !item.userId) {
return
}
if (!map.has(item.userId)) {
map.set(item.userId, item)
} else {
// infoNumber
const existing = map.get(item.userId)
if (!existing.infoNumber && item.infoNumber) {
// infoNumber
map.set(item.userId, item)
} else if (existing.infoNumber && !item.infoNumber) {
// infoNumber
//
}
}
})
return Array.from(map.values())
}, },
async loadUserData() { async loadUserData() {
if (!this.selectedUserId) { if (!this.selectedUserId) {
@ -184,7 +332,7 @@ export default {
// //
const rawScales = (this.userSummary && Array.isArray(this.userSummary.scales)) ? this.userSummary.scales : [] const rawScales = (this.userSummary && Array.isArray(this.userSummary.scales)) ? this.userSummary.scales : []
this.reportOptions = rawScales.reduce((result, scale) => { const scaleReports = rawScales.reduce((result, scale) => {
const attempts = Array.isArray(scale.attempts) ? scale.attempts : [] const attempts = Array.isArray(scale.attempts) ? scale.attempts : []
const reportRows = attempts const reportRows = attempts
.filter((attempt) => attempt && attempt.reportId) .filter((attempt) => attempt && attempt.reportId)
@ -198,10 +346,18 @@ export default {
submitTime: attempt.submitTime || attempt.startTime, submitTime: attempt.submitTime || attempt.startTime,
totalScore: attempt.totalScore, totalScore: attempt.totalScore,
summary: attempt.reportSummary || '', summary: attempt.reportSummary || '',
status: attempt.status status: attempt.status,
sourceType: 'assessment'
})) }))
return result.concat(reportRows) return result.concat(reportRows)
}, []) }, [])
const questionnaireReports = await this.loadQuestionnaireReports(this.selectedUserId)
const combinedReports = [...scaleReports, ...questionnaireReports].sort((a, b) => {
const timeA = a.submitTime ? new Date(a.submitTime).getTime() : 0
const timeB = b.submitTime ? new Date(b.submitTime).getTime() : 0
return timeB - timeA
})
this.reportOptions = combinedReports
this.selectedReports = [] this.selectedReports = []
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.reportTable && this.$refs.reportTable.doLayout) { if (this.$refs.reportTable && this.$refs.reportTable.doLayout) {
@ -226,12 +382,44 @@ export default {
}, },
resetSelection() { resetSelection() {
this.selectedUserId = undefined this.selectedUserId = undefined
this.userSearchKeyword = ''
this.userProfile = null this.userProfile = null
this.userSummary = null this.userSummary = null
this.reportOptions = [] this.reportOptions = []
this.selectedReports = [] this.selectedReports = []
this.comprehensiveReport = '' this.comprehensiveReport = ''
}, },
async loadQuestionnaireReports(userId) {
if (!userId) {
return []
}
try {
const response = await listReport({
userId: userId,
sourceType: 'questionnaire',
isGenerated: '1',
pageNum: 1,
pageSize: 1000
})
const rows = response.rows || []
return rows.map((row) => ({
key: `questionnaire-${row.reportId}`,
scaleId: row.sourceId,
scaleName: row.reportTitle || '问卷报告',
assessmentId: row.sourceId,
reportId: row.reportId,
reportTitle: row.reportTitle || '问卷报告',
submitTime: row.generateTime || row.createTime,
totalScore: row.totalScore || row.score || '-',
summary: row.summary || '',
status: row.isGenerated === '1' ? '1' : '0',
sourceType: 'questionnaire'
}))
} catch (error) {
console.error('加载问卷报告失败:', error)
return []
}
},
formatDateTime(value) { formatDateTime(value) {
if (!value) return '-' if (!value) return '-'
return parseTime(value) return parseTime(value)
@ -294,18 +482,20 @@ export default {
continue continue
} }
try { try {
const response = await getReport(row.reportId, 'assessment') const sourceType = row.sourceType === 'questionnaire' ? 'questionnaire' : 'assessment'
const response = await getReport(row.reportId, sourceType)
if (response && response.data) { if (response && response.data) {
reports.push({ reports.push({
scaleName: row.scaleName, scaleName: row.scaleName || row.reportTitle || '问卷报告',
submitTime: row.submitTime, submitTime: row.submitTime,
totalScore: row.totalScore, totalScore: row.totalScore,
summary: response.data.summary || row.summary || '', summary: response.data.summary || row.summary || '',
content: response.data.reportContent || '' content: response.data.reportContent || '',
sourceType
}) })
} }
} catch (error) { } catch (error) {
console.warn(`获取量表 ${row.scaleName} 的报告失败:`, error) console.warn(`获取${row.sourceType === 'questionnaire' ? '问卷' : '量表'} ${row.scaleName} 的报告失败:`, error)
} }
} }
return reports return reports
@ -357,12 +547,13 @@ export default {
const scaleReportsText = scaleReports const scaleReportsText = scaleReports
.map((report, index) => { .map((report, index) => {
const typeLabel = report.sourceType === 'questionnaire' ? '问卷' : '量表'
const contentText = report.content const contentText = report.content
.replace(/<[^>]*>/g, '') .replace(/<[^>]*>/g, '')
.replace(/&nbsp;/g, ' ') .replace(/&nbsp;/g, ' ')
.substring(0, 500) .substring(0, 500)
return ` return `
量表${index + 1}${report.scaleName} ${typeLabel}${index + 1}${report.scaleName}
测评时间${this.formatDateTime(report.submitTime)} 测评时间${this.formatDateTime(report.submitTime)}
总分${report.totalScore} 总分${report.totalScore}
报告摘要${report.summary || '无'} 报告摘要${report.summary || '无'}
@ -373,6 +564,7 @@ export default {
return `${SYSTEM_PROMPT}\n\n${userInfoText}\n\n${scaleReportsText}` return `${SYSTEM_PROMPT}\n\n${userInfoText}\n\n${scaleReportsText}`
}, },
// OLLAMA API
async callOLLAMA(prompt) { async callOLLAMA(prompt) {
try { try {
const { data } = await axios.post(this.OLLAMA_URL, { const { data } = await axios.post(this.OLLAMA_URL, {
@ -397,13 +589,13 @@ export default {
.trim() .trim()
if (!response) { if (!response) {
throw new Error('AI分析返回结果为空') throw new Error('本地AI分析返回结果为空')
} }
return response return response
} catch (error) { } catch (error) {
console.error('AI分析失败:', error) console.error('本地AI分析失败:', error)
throw new Error('AI分析失败' + (error.message || '未知错误')) throw new Error('本地AI分析失败' + (error.message || '未知错误'))
} }
}, },
formatReport(aiReport, userInfo, scaleReports) { formatReport(aiReport, userInfo, scaleReports) {
@ -449,7 +641,8 @@ export default {
</div> </div>
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; text-align: right; color: #909399; font-size: 12px;"> <div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; text-align: right; color: #909399; font-size: 12px;">
报告生成时间${parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}')} <div>报告生成时间${parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}')}</div>
<div style="margin-top: 8px;">被评估人<span style="color: #303133; font-weight: bold;">${userInfo.userName || '未知'}</span></div>
</div> </div>
</div> </div>
` `
@ -477,13 +670,8 @@ export default {
return html return html
}, },
exportReport(format) { buildReportHtml() {
if (!this.comprehensiveReport) { return `
this.$message.warning('报告内容为空')
return
}
const reportHtml = `
<html> <html>
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@ -505,6 +693,14 @@ export default {
</body> </body>
</html> </html>
` `
},
exportReport(format) {
if (!this.comprehensiveReport) {
this.$message.warning('报告内容为空')
return
}
const reportHtml = this.buildReportHtml()
if (format === 'word') { if (format === 'word') {
const blob = new Blob(['\ufeff', reportHtml], { type: 'application/msword' }) const blob = new Blob(['\ufeff', reportHtml], { type: 'application/msword' })
@ -518,6 +714,15 @@ export default {
window.URL.revokeObjectURL(link.href) window.URL.revokeObjectURL(link.href)
this.$message.success('导出成功') this.$message.success('导出成功')
} else { } else {
this.printReport()
}
},
printReport() {
if (!this.comprehensiveReport) {
this.$message.warning('报告内容为空')
return
}
const reportHtml = this.buildReportHtml()
const printWindow = window.open('', '_blank') const printWindow = window.open('', '_blank')
if (!printWindow) { if (!printWindow) {
this.$message.error('无法打开打印窗口,请检查浏览器是否阻止了弹窗') this.$message.error('无法打开打印窗口,请检查浏览器是否阻止了弹窗')
@ -530,7 +735,6 @@ export default {
} }
} }
} }
}
</script> </script>
<style scoped> <style scoped>

View File

@ -2,20 +2,38 @@
<div class="app-container"> <div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px"> <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
<el-form-item label="量表名称" prop="scaleName"> <el-form-item label="量表名称" prop="scaleName">
<el-input <el-select
v-model="queryParams.scaleName" v-model="queryParams.scaleName"
placeholder="请输入量表名称" placeholder="请选择或输入量表名称"
clearable clearable
@keyup.enter.native="handleQuery" filterable
allow-create
default-first-option
>
<el-option
v-for="name in uniqueScaleNames"
:key="name"
:label="name"
:value="name"
/> />
</el-select>
</el-form-item> </el-form-item>
<el-form-item label="量表编码" prop="scaleCode"> <el-form-item label="量表编码" prop="scaleCode">
<el-input <el-select
v-model="queryParams.scaleCode" v-model="queryParams.scaleCode"
placeholder="请输入量表编码" placeholder="请选择或输入量表编码"
clearable clearable
@keyup.enter.native="handleQuery" filterable
allow-create
default-first-option
>
<el-option
v-for="code in uniqueScaleCodes"
:key="code"
:label="code"
:value="code"
/> />
</el-select>
</el-form-item> </el-form-item>
<el-form-item label="量表类型" prop="scaleType"> <el-form-item label="量表类型" prop="scaleType">
<el-select v-model="queryParams.scaleType" placeholder="量表类型" clearable> <el-select v-model="queryParams.scaleType" placeholder="量表类型" clearable>
@ -30,7 +48,7 @@
<el-form-item label="状态" prop="status"> <el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="状态" clearable> <el-select v-model="queryParams.status" placeholder="状态" clearable>
<el-option <el-option
v-for="dict in dict.type.psy_scale_status" v-for="dict in scaleStatusOptions"
:key="dict.value" :key="dict.value"
:label="dict.label" :label="dict.label"
:value="dict.value" :value="dict.value"
@ -427,6 +445,95 @@
<p style="margin-top: 20px; color: #606266;">正在生成二维码...</p> <p style="margin-top: 20px; color: #606266;">正在生成二维码...</p>
</div> </div>
</el-dialog> </el-dialog>
<!-- 添加或修改问卷对话框复用问卷管理的对话框 -->
<el-dialog :title="questionnaireTitle" :visible.sync="questionnaireOpen" width="900px" append-to-body>
<el-form ref="questionnaireForm" :model="questionnaireForm" :rules="questionnaireRules" label-width="120px">
<el-row>
<el-col :span="12">
<el-form-item label="问卷编码" prop="questionnaireCode">
<el-input v-model="questionnaireForm.questionnaireCode" placeholder="请输入问卷编码" :disabled="questionnaireForm.questionnaireId != undefined" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="问卷名称" prop="questionnaireName">
<el-input v-model="questionnaireForm.questionnaireName" placeholder="请输入问卷名称" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="问卷类型" prop="questionnaireType">
<el-select v-model="questionnaireForm.questionnaireType" placeholder="请选择问卷类型">
<el-option label="自定义" value="custom" />
<el-option label="考试" value="exam" />
<el-option label="练习" value="practice" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="组卷方式" prop="paperType">
<el-select v-model="questionnaireForm.paperType" placeholder="请选择组卷方式">
<el-option label="手动" value="manual" />
<el-option label="随机" value="random" />
<el-option label="混合" value="mixed" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="题目数量" prop="itemCount">
<el-input-number v-model="questionnaireForm.itemCount" :min="0" controls-position="right" disabled />
<span style="color: #909399; font-size: 12px; margin-left: 10px;">根据题目自动计算</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="总分" prop="totalScore">
<el-input-number v-model="questionnaireForm.totalScore" :min="0" :precision="2" controls-position="right" disabled />
<span style="color: #909399; font-size: 12px; margin-left: 10px;">根据题目分数自动计算</span>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="及格分数" prop="passScore">
<el-input-number v-model="questionnaireForm.passScore" :min="0" :precision="2" controls-position="right" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预计时间(分)" prop="estimatedTime">
<el-input-number v-model="questionnaireForm.estimatedTime" :min="1" controls-position="right" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="问卷描述" prop="description">
<el-input v-model="questionnaireForm.description" type="textarea" :rows="4" placeholder="请输入问卷描述" />
</el-form-item>
<el-row>
<el-col :span="12">
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="questionnaireForm.sortOrder" :min="0" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-radio-group v-model="questionnaireForm.status">
<el-radio label="0">正常</el-radio>
<el-radio label="1">停用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注">
<el-input v-model="questionnaireForm.remark" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitQuestionnaireForm"> </el-button>
<el-button @click="cancelQuestionnaire"> </el-button>
</div>
</el-dialog>
</div> </div>
</template> </template>
@ -454,6 +561,8 @@ export default {
total: 0, total: 0,
// //
scaleList: [], scaleList: [],
//
allScaleList: [],
// //
title: "", title: "",
// //
@ -474,6 +583,21 @@ export default {
// //
qrcodeOpen: false, qrcodeOpen: false,
qrcodeInfo: null, qrcodeInfo: null,
//
questionnaireOpen: false,
questionnaireTitle: "修改问卷",
questionnaireForm: {},
questionnaireRules: {
questionnaireCode: [
{ required: true, message: "问卷编码不能为空", trigger: "blur" }
],
questionnaireName: [
{ required: true, message: "问卷名称不能为空", trigger: "blur" }
],
questionnaireType: [
{ required: true, message: "问卷类型不能为空", trigger: "change" }
]
},
// //
queryParams: { queryParams: {
pageNum: 1, pageNum: 1,
@ -481,7 +605,8 @@ export default {
scaleName: undefined, scaleName: undefined,
scaleCode: undefined, scaleCode: undefined,
scaleType: undefined, scaleType: undefined,
status: undefined status: undefined,
includeQuestionnaire: true //
}, },
// //
form: {}, form: {},
@ -513,10 +638,50 @@ export default {
seen.add(dict.value); seen.add(dict.value);
return true; return true;
}); });
},
/** 状态下拉选项,缺失字典时使用本地默认值 */
scaleStatusOptions() {
const dictList = (this.dict && this.dict.type && this.dict.type.psy_scale_status) || [];
if (dictList.length) {
return dictList;
}
return [
{ label: "正常", value: "0" },
{ label: "停用", value: "1" }
];
},
/** 去重后的量表名称列表 */
uniqueScaleNames() {
// 使
if (!this.allScaleList || this.allScaleList.length === 0) {
return [];
}
const nameSet = new Set();
this.allScaleList.forEach(scale => {
if (scale.scaleName && scale.scaleName.trim()) {
nameSet.add(scale.scaleName.trim());
}
});
return Array.from(nameSet).sort();
},
/** 去重后的量表编码列表 */
uniqueScaleCodes() {
// 使
if (!this.allScaleList || this.allScaleList.length === 0) {
return [];
}
const codeSet = new Set();
this.allScaleList.forEach(scale => {
if (scale.scaleCode && scale.scaleCode.trim()) {
codeSet.add(scale.scaleCode.trim());
}
});
return Array.from(codeSet).sort();
} }
}, },
created() { created() {
this.getList() this.getAllScaleList() //
this.getList() //
}, },
methods: { methods: {
/** 获取字典标签 */ /** 获取字典标签 */
@ -525,6 +690,21 @@ export default {
const dict = dictList.find(item => item.value === value); const dict = dictList.find(item => item.value === value);
return dict ? dict.label : value; return dict ? dict.label : value;
}, },
/** 获取所有量表列表(不分页,用于下拉框) */
getAllScaleList() {
//
const allParams = {
pageNum: 1,
pageSize: 10000, //
includeQuestionnaire: true //
}
listScale(allParams).then(response => {
this.allScaleList = response.rows || []
}).catch(error => {
console.error('获取所有量表列表失败:', error)
this.allScaleList = []
})
},
/** 查询量表列表 */ /** 查询量表列表 */
getList() { getList() {
this.loading = true this.loading = true
@ -610,12 +790,14 @@ export default {
updateScale(this.form).then(response => { updateScale(this.form).then(response => {
this.$modal.msgSuccess("修改成功") this.$modal.msgSuccess("修改成功")
this.open = false this.open = false
this.getAllScaleList() //
this.getList() this.getList()
}) })
} else { } else {
addScale(this.form).then(response => { addScale(this.form).then(response => {
this.$modal.msgSuccess("新增成功") this.$modal.msgSuccess("新增成功")
this.open = false this.open = false
this.getAllScaleList() //
this.getList() this.getList()
}) })
} }
@ -628,6 +810,7 @@ export default {
this.$modal.confirm('是否确认删除量表编号为"' + scaleIds + '"的数据项?').then(function() { this.$modal.confirm('是否确认删除量表编号为"' + scaleIds + '"的数据项?').then(function() {
return delScale(scaleIds) return delScale(scaleIds)
}).then(() => { }).then(() => {
this.getAllScaleList() //
this.getList() this.getList()
this.$modal.msgSuccess("删除成功") this.$modal.msgSuccess("删除成功")
}).catch(() => {}) }).catch(() => {})
@ -779,6 +962,7 @@ export default {
this.$modal.msgSuccess(response.msg || "导入成功") this.$modal.msgSuccess(response.msg || "导入成功")
this.importOpen = false this.importOpen = false
this.importJsonText = "" this.importJsonText = ""
this.getAllScaleList() //
this.getList() this.getList()
}).catch(error => { }).catch(error => {
this.$modal.msgError(error.msg || "导入失败") this.$modal.msgError(error.msg || "导入失败")
@ -815,6 +999,7 @@ export default {
this.upload.fileContent = null this.upload.fileContent = null
this.$refs.upload.clearFiles() this.$refs.upload.clearFiles()
this.upload.isUploading = false this.upload.isUploading = false
this.getAllScaleList() //
this.getList() this.getList()
}).catch(error => { }).catch(error => {
this.$modal.msgError(error.msg || "导入失败") this.$modal.msgError(error.msg || "导入失败")
@ -847,6 +1032,7 @@ export default {
this.upload.fileContent = null this.upload.fileContent = null
this.$refs.upload.clearFiles() this.$refs.upload.clearFiles()
this.upload.isUploading = false this.upload.isUploading = false
this.getAllScaleList() //
this.getList() this.getList()
}).catch(error => { }).catch(error => {
this.$modal.msgError(error.msg || "导入失败") this.$modal.msgError(error.msg || "导入失败")
@ -916,8 +1102,62 @@ export default {
/** 问卷修改按钮操作 */ /** 问卷修改按钮操作 */
handleQuestionnaireUpdate(row) { handleQuestionnaireUpdate(row) {
const questionnaireId = row.originalId || Math.abs(row.scaleId) const questionnaireId = row.originalId || Math.abs(row.scaleId)
// this.resetQuestionnaireForm()
this.$router.push({ path: '/psychology/questionnaire', query: { questionnaireId: questionnaireId, action: 'edit' } }) getQuestionnaire(questionnaireId).then(response => {
this.questionnaireForm = response.data
this.questionnaireOpen = true
this.questionnaireTitle = "修改问卷"
}).catch(error => {
console.error("获取问卷信息失败:", error)
this.$modal.msgError("获取问卷信息失败")
})
},
/** 提交问卷表单 */
submitQuestionnaireForm() {
this.$refs["questionnaireForm"].validate(valid => {
if (valid) {
if (this.questionnaireForm.questionnaireId != undefined) {
updateQuestionnaire(this.questionnaireForm).then(response => {
this.$modal.msgSuccess("修改成功")
this.questionnaireOpen = false
this.resetQuestionnaireForm()
//
this.getAllScaleList() //
this.getList()
}).catch(error => {
console.error("修改问卷失败:", error)
})
}
}
})
},
/** 取消问卷编辑 */
cancelQuestionnaire() {
this.questionnaireOpen = false
this.resetQuestionnaireForm()
},
/** 重置问卷表单 */
resetQuestionnaireForm() {
this.questionnaireForm = {
questionnaireId: undefined,
questionnaireCode: undefined,
questionnaireName: undefined,
questionnaireType: "custom",
paperType: "manual",
itemCount: 0,
totalScore: undefined,
passScore: undefined,
estimatedTime: undefined,
description: undefined,
status: "0",
sortOrder: 0,
remark: undefined
}
this.$nextTick(() => {
if (this.$refs.questionnaireForm) {
this.$refs.questionnaireForm.clearValidate()
}
})
}, },
/** 问卷删除按钮操作 */ /** 问卷删除按钮操作 */
handleQuestionnaireDelete(row) { handleQuestionnaireDelete(row) {
@ -926,6 +1166,7 @@ export default {
this.$modal.confirm('是否确认删除问卷"' + questionnaireName + '"的数据项?').then(() => { this.$modal.confirm('是否确认删除问卷"' + questionnaireName + '"的数据项?').then(() => {
return delQuestionnaire(questionnaireId) return delQuestionnaire(questionnaireId)
}).then(() => { }).then(() => {
this.getAllScaleList() //
this.getList() this.getList()
this.$modal.msgSuccess("删除成功") this.$modal.msgSuccess("删除成功")
}).catch(() => {}) }).catch(() => {})

View File

@ -45,7 +45,12 @@
{{ test.description }} {{ test.description }}
</div> </div>
<div class="test-action"> <div class="test-action">
<el-button type="primary" size="small" @click.stop="handleStartTest(test)">开始测试</el-button> <el-button
:type="hasPaused(test.scaleId) ? 'warning' : 'primary'"
size="small"
@click.stop="handleStartTest(test)">
{{ hasPaused(test.scaleId) ? '继续测试' : '开始测试' }}
</el-button>
</div> </div>
</div> </div>
</el-card> </el-card>
@ -63,7 +68,7 @@
* 作用显示所有开放的心理测试题学员可以选择进行测试 * 作用显示所有开放的心理测试题学员可以选择进行测试
*/ */
import { listScale } from "@/api/psychology/scale" import { listScale } from "@/api/psychology/scale"
import { startAssessment } from "@/api/psychology/assessment" import { startAssessment, pausedAssessmentList, resumeAssessment } from "@/api/psychology/assessment"
import { Message } from 'element-ui' import { Message } from 'element-ui'
export default { export default {
@ -72,7 +77,8 @@ export default {
return { return {
loading: false, loading: false,
testList: [], testList: [],
searchText: "" searchText: "",
pausedMap: {}
} }
}, },
computed: { computed: {
@ -97,16 +103,19 @@ export default {
// //
if (to.path === '/student/tests') { if (to.path === '/student/tests') {
this.loadTestList() this.loadTestList()
this.loadPausedList()
} }
} }
}, },
created() { created() {
// //
this.loadTestList() this.loadTestList()
this.loadPausedList()
}, },
// //
activated() { activated() {
this.loadTestList() this.loadTestList()
this.loadPausedList()
}, },
methods: { methods: {
// //
@ -132,6 +141,7 @@ export default {
}) })
this.loading = false this.loading = false
this.loadPausedList()
}).catch(error => { }).catch(error => {
console.error("loadTestList, 加载测试题列表失败:", error) console.error("loadTestList, 加载测试题列表失败:", error)
Message.error("加载测试题列表失败,请稍后重试") Message.error("加载测试题列表失败,请稍后重试")
@ -139,6 +149,33 @@ export default {
}) })
}, },
loadPausedList() {
pausedAssessmentList()
.then(response => {
const rows = response.data || []
const map = {}
rows.forEach(item => {
if (!item || !item.scaleId) {
return
}
const existing = map[item.scaleId]
if (!existing) {
map[item.scaleId] = item
} else {
const existingTime = new Date(existing.pauseTime || existing.startTime || 0).getTime()
const currentTime = new Date(item.pauseTime || item.startTime || 0).getTime()
if (currentTime > existingTime) {
map[item.scaleId] = item
}
}
})
this.pausedMap = map
})
.catch(error => {
console.error("加载暂停测评失败:", error)
})
},
// //
handleSearch() { handleSearch() {
// computed // computed
@ -152,6 +189,28 @@ export default {
return return
} }
const pausedRecord = this.getPausedRecord(test.scaleId)
if (pausedRecord) {
//
this.$confirm('检测到您有该量表的暂停测评,是否继续?', '提示', {
confirmButtonText: '继续测评',
cancelButtonText: '重新开始',
type: 'warning'
}).then(() => {
this.continueAssessment(pausedRecord)
}).catch(() => {
//
this.$confirm('重新开始将清空之前的答题记录,确定要重新开始吗?', '警告', {
confirmButtonText: '确定重新开始',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.createNewAssessment(test)
})
})
return
}
// //
if (test.sourceType === 'questionnaire') { if (test.sourceType === 'questionnaire') {
const questionnaireId = test.originalId || Math.abs(test.scaleId) const questionnaireId = test.originalId || Math.abs(test.scaleId)
@ -166,6 +225,11 @@ export default {
this.createAssessment(test) this.createAssessment(test)
}, },
//
createNewAssessment(test) {
this.createAssessment(test)
},
// //
createAssessment(test) { createAssessment(test) {
this.loading = true this.loading = true
@ -203,6 +267,33 @@ export default {
}) })
}, },
getPausedRecord(scaleId) {
return this.pausedMap[scaleId] || null
},
hasPaused(scaleId) {
return !!this.getPausedRecord(scaleId)
},
continueAssessment(record) {
if (!record || !record.assessmentId) {
return
}
this.loading = true
resumeAssessment(record.assessmentId)
.catch(() => {
// 使
})
.finally(() => {
this.loading = false
this.$router.push({
path: '/psychology/assessment/taking',
query: { assessmentId: record.assessmentId }
})
this.loadPausedList()
})
},
// //
getScaleTypeName(type) { getScaleTypeName(type) {
// //

View File

@ -251,7 +251,7 @@
<!-- 用户导入对话框 --> <!-- 用户导入对话框 -->
<el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body> <el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
<el-upload ref="upload" :limit="1" accept=".xlsx, .xls" :headers="upload.headers" :action="upload.url + '?updateSupport=' + upload.updateSupport" :disabled="upload.isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :on-error="handleFileError" :auto-upload="false" drag> <el-upload ref="upload" :limit="1" accept=".xlsx, .xls" :headers="upload.headers" :action="upload.url + '?updateSupport=' + upload.updateSupport" :disabled="upload.isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag>
<i class="el-icon-upload"></i> <i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div> <div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<div class="el-upload__tip text-center" slot="tip"> <div class="el-upload__tip text-center" slot="tip">
@ -666,14 +666,6 @@ export default {
this.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true }) this.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true })
this.getList() this.getList()
}, },
//
handleFileError(err, file, fileList) {
this.upload.isUploading = false
if (this.$refs.upload) {
this.$refs.upload.clearFiles()
}
this.$modal.msgError((err && (err.msg || err.message)) || "文件上传失败,请稍后重试或检查网络连接。")
},
// //
submitFileForm() { submitFileForm() {
const file = this.$refs.upload.uploadFiles const file = this.$refs.upload.uploadFiles

View File

@ -35,7 +35,7 @@ module.exports = {
transpileDependencies: ['quill'], transpileDependencies: ['quill'],
// webpack-dev-server 相关配置 // webpack-dev-server 相关配置
devServer: { devServer: {
host: '0.0.0.0', host: '127.0.0.1',
port: port, port: port,
open: true, open: true,
proxy: { proxy: {