修改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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ddnai.common.annotation.Log;
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.IPsyScaleItemService;
import java.util.ArrayList;
import java.math.BigDecimal;
/**
* 测评记录 信息操作处理
@ -87,10 +87,33 @@ public class PsyAssessmentController extends BaseController
* 获取暂停的测评列表
*/
@GetMapping("/pausedList")
public AjaxResult pausedList()
public AjaxResult pausedList(@RequestParam(value = "userId", required = false) Long userId)
{
Long userId = SecurityUtils.getUserId();
List<PsyAssessment> list = assessmentService.selectPausedAssessmentList(userId);
Long currentUserId = SecurityUtils.getUserId();
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);
}

View File

@ -1,7 +1,6 @@
package com.ddnai.web.controller.psychology;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
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)
{
Set<Long> allowedScaleIds = resolveAllowedScaleIdsForCurrentUser();
Set<Long> restrictedScaleIds = resolveRestrictedScaleIds();
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();
@ -119,7 +119,7 @@ public class PsyScaleController extends BaseController
scaleList.add(scaleItem);
}
scaleList = filterScaleListByPermission(scaleList, allowedScaleIds);
scaleList = filterScaleListByPermission(scaleList, allowedScaleIds, restrictedScaleIds);
// 按排序顺序和创建时间排序
scaleList.sort((a, b) -> {
@ -157,7 +157,7 @@ public class PsyScaleController extends BaseController
}
}
scaleList = filterScaleListByPermission(scaleList, allowedScaleIds);
scaleList = filterScaleListByPermission(scaleList, allowedScaleIds, restrictedScaleIds);
if (needPermissionFilter)
{
@ -193,7 +193,6 @@ public class PsyScaleController extends BaseController
scale.setRemark(questionnaire.getRemark());
return scale;
}
/**
* 构建手动分页结果适用于自定义过滤后的列表
*/
@ -224,7 +223,7 @@ public class PsyScaleController extends BaseController
* @param scaleList 原始列表
* @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)
{
@ -238,13 +237,16 @@ public class PsyScaleController extends BaseController
continue;
}
String sourceType = scale.getSourceType();
Long scaleId = scale.getScaleId();
if ("questionnaire".equalsIgnoreCase(sourceType))
{
// 问卷类型默认对所有学员开放
filtered.add(scale);
continue;
boolean restricted = restrictedScaleIds != null && restrictedScaleIds.contains(scaleId);
if (!restricted)
{
filtered.add(scale);
continue;
}
}
Long scaleId = scale.getScaleId();
if (scaleId != null && allowedScaleIds.contains(scaleId))
{
filtered.add(scale);
@ -585,6 +587,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")
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.StringUtils;
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.service.ISysDeptService;
import com.ddnai.system.service.ISysPostService;
@ -292,4 +293,15 @@ public class PsyUserProfileController extends BaseController
String message = profileService.importProfile(profileList, updateSupport, operName);
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;
/** 来源类型scale/questionnaire */
private String sourceType;
/** 部门名称(关联查询字段,不存储在表中) */
private String deptName;
@ -155,6 +158,16 @@ public class PsyScalePermission extends BaseEntity
this.scaleName = scaleName;
}
public String getSourceType()
{
return sourceType;
}
public void setSourceType(String sourceType)
{
this.sourceType = sourceType;
}
public String getDeptName()
{
return deptName;
@ -202,6 +215,8 @@ public class PsyScalePermission extends BaseEntity
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.append("scaleName", getScaleName())
.append("sourceType", getSourceType())
.toString();
}
}

View File

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

View File

@ -91,5 +91,12 @@ public interface PsyScalePermissionMapper
* @return 结果
*/
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.system.domain.psychology.PsyScalePermission;
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.IPsyScaleService;
import com.ddnai.system.service.psychology.IPsyQuestionnaireService;
/**
* 量表权限 服务层实现
@ -27,6 +29,9 @@ public class PsyScalePermissionServiceImpl implements IPsyScalePermissionService
@Autowired
private IPsyScaleService scaleService;
@Autowired
private IPsyQuestionnaireService questionnaireService;
/**
* 查询量表权限信息
@ -92,15 +97,7 @@ public class PsyScalePermissionServiceImpl implements IPsyScalePermissionService
@Override
public int insertPermission(PsyScalePermission permission)
{
// 验证 scale_id 是否存在
if (permission.getScaleId() != null)
{
com.ddnai.system.domain.psychology.PsyScale scale = scaleService.selectScaleById(permission.getScaleId());
if (scale == null)
{
throw new RuntimeException("量表不存在scaleId: " + permission.getScaleId());
}
}
validateScaleExists(permission.getScaleId());
if (permission.getStatus() == null)
{
@ -150,15 +147,7 @@ public class PsyScalePermissionServiceImpl implements IPsyScalePermissionService
@Override
public int updatePermission(PsyScalePermission permission)
{
// 验证 scale_id 是否存在
if (permission.getScaleId() != null)
{
com.ddnai.system.domain.psychology.PsyScale scale = scaleService.selectScaleById(permission.getScaleId());
if (scale == null)
{
throw new RuntimeException("量表不存在scaleId: " + permission.getScaleId());
}
}
validateScaleExists(permission.getScaleId());
return permissionMapper.updatePermission(permission);
}
@ -239,12 +228,10 @@ public class PsyScalePermissionServiceImpl implements IPsyScalePermissionService
}
try {
// 验证 scale_id 是否存在
com.ddnai.system.domain.psychology.PsyScale scale = scaleService.selectScaleById(scaleId);
if (scale == null)
if (!validateScaleExistsSilent(scaleId))
{
log.warn("量表不存在跳过该权限分配scaleId: {}", scaleId);
continue; // 跳过不存在的量表
log.warn("量表/问卷不存在跳过该权限分配scaleId: {}", scaleId);
continue;
}
PsyScalePermission permission = new PsyScalePermission();
@ -267,5 +254,35 @@ public class PsyScalePermissionServiceImpl implements IPsyScalePermissionService
}
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.system.domain.psychology.PsyUserProfile;
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.ISysRoleService;
import com.ddnai.system.service.ISysUserService;
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
private ISysRoleService roleService;
@Autowired
private ImportProgressManager importProgressManager;
/**
* 查询档案信息
*
@ -134,14 +139,14 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
profile.setInfoNumber(infoNumber);
validateInfoNumberUnique(infoNumber, null);
// 验证姓名如果提供只能包含汉字
// 验证姓名如果提供只能包含汉字和数字
if (StringUtils.isNotEmpty(profile.getUserName()))
{
String userName = profile.getUserName().trim();
if (!userName.matches("^[\\u4e00-\\u9fa5]+$"))
if (!userName.matches("^[\\u4e00-\\u9fa5\\d]+$"))
{
log.error("创建用户档案失败:姓名格式错误,只能输入汉字userName: {}", userName);
throw new ServiceException("姓名只能输入汉字");
log.error("创建用户档案失败:姓名格式错误,只能输入汉字和数字userName: {}", userName);
throw new ServiceException("姓名只能输入汉字和数字");
}
profile.setUserName(userName);
}
@ -302,14 +307,14 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
profile.setInfoNumber(infoNumber);
validateInfoNumberUnique(infoNumber, profile.getProfileId());
// 验证姓名如果提供只能包含汉字
// 验证姓名如果提供只能包含汉字和数字
if (StringUtils.isNotEmpty(profile.getUserName()))
{
String userName = profile.getUserName().trim();
if (!userName.matches("^[\\u4e00-\\u9fa5]+$"))
if (!userName.matches("^[\\u4e00-\\u9fa5\\d]+$"))
{
log.error("修改用户档案失败:姓名格式错误,只能输入汉字userName: {}", userName);
throw new ServiceException("姓名只能输入汉字");
log.error("修改用户档案失败:姓名格式错误,只能输入汉字和数字userName: {}", userName);
throw new ServiceException("姓名只能输入汉字和数字");
}
profile.setUserName(userName);
syncUserName(profile.getUserId(), userName);
@ -488,92 +493,133 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
{
throw new ServiceException("导入用户档案数据不能为空!");
}
final String progressKey = importProgressManager.buildProfileKey(operName);
importProgressManager.start(progressKey, profileList.size());
int successNum = 0;
int failureNum = 0;
StringBuilder successMsg = new StringBuilder();
StringBuilder failureMsg = new StringBuilder();
for (PsyUserProfile profile : profileList)
try
{
try
for (PsyUserProfile profile : profileList)
{
// 设置创建者
profile.setCreateBy(operName);
// 验证信息编号
if (StringUtils.isEmpty(profile.getInfoNumber()))
try
{
// 设置创建者
profile.setCreateBy(operName);
// 验证信息编号
if (StringUtils.isEmpty(profile.getInfoNumber()))
{
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、档案信息编号为空");
importProgressManager.recordFailure(progressKey);
continue;
}
// 根据信息编号查询是否存在
PsyUserProfile existProfile = profileMapper.selectProfileByInfoNumber(profile.getInfoNumber());
if (StringUtils.isNull(existProfile))
{
// 新增档案
this.insertProfile(profile);
successNum++;
successMsg.append("<br/>").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 导入成功");
importProgressManager.recordSuccess(progressKey);
}
else if (isUpdateSupport)
{
// 更新档案
profile.setProfileId(existProfile.getProfileId());
profile.setUserId(existProfile.getUserId());
profile.setUpdateBy(operName);
this.updateProfile(profile);
successNum++;
successMsg.append("<br/>").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 更新成功");
importProgressManager.recordSuccess(progressKey);
}
else
{
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 已存在");
importProgressManager.recordFailure(progressKey);
}
}
catch (Exception e)
{
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、档案信息编号为空");
continue;
String msg = "<br/>" + failureNum + "、信息编号 " + profile.getInfoNumber() + " 导入失败:";
failureMsg.append(msg).append(e.getMessage());
importProgressManager.recordFailure(progressKey);
log.error(msg, e);
}
// 根据信息编号查询是否存在
PsyUserProfile existProfile = profileMapper.selectProfileByInfoNumber(profile.getInfoNumber());
if (StringUtils.isNull(existProfile))
}
// 根据成功/失败情况生成结果
if (failureNum > 0)
{
if (successNum == 0)
{
// 新增档案
this.insertProfile(profile);
successNum++;
successMsg.append("<br/>").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 导入成功");
}
else if (isUpdateSupport)
{
// 更新档案
profile.setProfileId(existProfile.getProfileId());
profile.setUserId(existProfile.getUserId());
profile.setUpdateBy(operName);
this.updateProfile(profile);
successNum++;
successMsg.append("<br/>").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 更新成功");
// 全部失败
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
String finalMsg = failureMsg.toString();
importProgressManager.finishFailure(progressKey, finalMsg);
throw new ServiceException(finalMsg);
}
else
{
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 已存在");
// 部分成功部分失败同时返回成功和失败统计
StringBuilder resultMsg = new StringBuilder();
resultMsg.append("本次导入完成:共 ")
.append(successNum + failureNum)
.append(" 条,其中成功 ")
.append(successNum)
.append(" 条,失败 ")
.append(failureNum)
.append(" 条。");
if (successNum > 0)
{
resultMsg.append("<br/>成功明细如下:");
resultMsg.append(successMsg);
}
failureMsg.insert(0, "<br/><br/>失败明细如下:");
resultMsg.append(failureMsg);
String finalMsg = resultMsg.toString();
importProgressManager.finishFailure(progressKey, finalMsg);
return finalMsg;
}
}
catch (Exception e)
{
failureNum++;
String msg = "<br/>" + failureNum + "、信息编号 " + profile.getInfoNumber() + " 导入失败:";
failureMsg.append(msg).append(e.getMessage());
log.error(msg, e);
}
}
if (failureNum > 0)
{
if (successNum == 0)
{
// 全部失败保持原有行为抛出异常
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
throw new ServiceException(failureMsg.toString());
}
else
{
// 部分成功部分失败同时返回成功和失败统计前端可一次性展示
StringBuilder resultMsg = new StringBuilder();
resultMsg.append("本次导入完成:共 ")
.append(successNum + failureNum)
.append(" 条,其中成功 ")
.append(successNum)
.append(" 条,失败 ")
.append(failureNum)
.append(" 条。成功明细如下:");
resultMsg.append(successMsg);
failureMsg.insert(0, "<br/><br/>失败明细如下:");
resultMsg.append(failureMsg);
return resultMsg.toString();
// 没有失败
if (successNum > 0)
{
String successMessage = "恭喜您,数据已全部导入成功!<br/>" + successMsg.toString();
importProgressManager.finishSuccess(progressKey, successMessage);
return successMessage;
}
// 理论上不会到这里既没有成功也没有失败但为安全起见保留兜底逻辑
importProgressManager.finishSuccess(progressKey, "导入完成,但未检测到需要处理的数据。");
return "导入完成,但未检测到需要处理的数据。";
}
else
catch (RuntimeException ex)
{
// 全部成功
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
return successMsg.toString();
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);
/**
* 查询所有已配置权限的量表ID
*
* @return 量表ID集合
*/
public List<Long> selectAllScaleIdsWithPermission();
/**
* 批量分配用户量表权限
*

View File

@ -1,6 +1,7 @@
package com.ddnai.system.service.psychology;
import java.util.List;
import com.ddnai.system.domain.dto.ImportProgress;
import com.ddnai.system.domain.psychology.PsyUserProfile;
/**
@ -83,5 +84,13 @@ public interface IPsyUserProfileService
* @return 结果
*/
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">
<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
</select>
@ -349,4 +354,3 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select>
</mapper>

View File

@ -26,6 +26,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
<result property="sourceType" column="source_type" />
</resultMap>
<sql id="selectScaleVo">
@ -34,7 +35,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
COALESCE(COUNT(i.item_id), 0) as item_count,
s.estimated_time, s.target_population,
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
left join psy_scale_item i on s.scale_id = i.scale_id
</sql>

View File

@ -20,6 +20,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
<result property="scaleName" column="scale_name" />
<result property="sourceType" column="source_type" />
<result property="deptName" column="dept_name" />
<result property="roleName" column="role_name" />
<result property="userName" column="user_name" />
@ -28,9 +29,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<sql id="selectPermissionVo">
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,
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
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_role r on p.role_id = r.role_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>
<select id="selectAllScaleIdsWithPermission" resultType="Long">
select distinct scale_id
from psy_scale_permission
where status = '0'
</select>
</mapper>

View File

@ -109,6 +109,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="crimeName != null and crimeName != ''">
AND p.crime_name like concat('%', #{crimeName}, '%')
</if>
<if test="status != null and status != ''">
AND u.status = #{status}
</if>
<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')
</if>
@ -211,8 +214,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectStudentProfileList" parameterType="com.ddnai.system.domain.psychology.PsyUserProfile" resultMap="PsyUserProfileResult">
select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar as user_avatar, u.phonenumber as phone, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by as user_create_by, u.create_time as user_create_time, u.remark as user_remark,
d.dept_name, d.leader,
p.profile_id, p.profile_type, p.profile_data, p.avatar, p.id_card, p.birthday,
p.prison, p.prison_area, p.gender, p.nation, p.education_level, p.crime_name,
p.profile_id, p.profile_type, p.profile_data, p.avatar, p.id_card, p.birthday,
p.prison, p.prison_area, p.gender, p.nation, p.education_level, p.crime_name,
p.sentence_term, p.sentence_start_date, p.sentence_end_date, p.entry_date,
p.info_number, p.create_by, p.create_time, p.update_by, p.update_time, p.remark
from sys_user u

View File

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

View File

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

View File

@ -41,7 +41,7 @@
icon="el-icon-plus"
size="mini"
@click="handleStartAssessment"
v-hasPermi="['psychology:assessment:add']"
v-hasPermi="['psychology:assessment:start']"
>开始测评</el-button>
</el-col>
<el-col :span="1.5">
@ -61,7 +61,11 @@
<el-table v-loading="loading" :data="assessmentList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<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="startTime" width="180">
<template slot-scope="scope">
@ -147,7 +151,9 @@
<el-descriptions :column="2" border style="margin-bottom: 20px;">
<el-descriptions-item label="测评ID">{{ currentAssessment.assessmentId }}</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="提交时间">
<span v-if="currentAssessment.submitTime">{{ parseTime(currentAssessment.submitTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
<span v-else>-</span>
@ -158,7 +164,6 @@
<el-divider content-position="left">答题详情</el-divider>
<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="itemContent" min-width="200" :show-overflow-tooltip="true" />
<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() {
// URLscaleIdprofileId/userId
const scaleId = this.$route.query.scaleId;
@ -130,7 +135,8 @@ export default {
this.loadScales();
this.loadProfiles();
this.loadPaused();
const initialPausedUserId = this.targetUserId ? parseInt(this.targetUserId) : this.$store.getters.id;
this.loadPaused(initialPausedUserId);
},
methods: {
/** 加载量表列表 */
@ -139,8 +145,9 @@ export default {
const userId = this.$store.getters.id;
const roles = this.$store.getters.roles || [];
// userId === 1 roles 'admin'
const isAdmin = userId === 1 || (roles && roles.includes('admin'));
// admin
//
const isAdmin = roles && roles.includes('admin');
//
if (isAdmin) {
@ -284,11 +291,30 @@ export default {
})
},
/** 加载暂停的测评 */
loadPaused() {
pausedAssessmentList().then(response => {
loadPaused(targetUserId) {
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 || [];
});
},
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() {
//
@ -374,6 +400,15 @@ export default {
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;
//

View File

@ -38,10 +38,11 @@
size="small"
@click="speakText(currentItem.itemContent)"
:disabled="!isTtsSupported"
class="tts-btn"
:class="['tts-btn', isSpeaking ? 'speaking' : '']"
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>
</div>
@ -55,10 +56,11 @@
size="mini"
@click="speakText(option.optionContent)"
:disabled="!isTtsSupported"
class="option-tts-btn"
:class="['option-tts-btn', isSpeaking ? 'speaking' : '']"
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>
</div>
</el-radio-group>
@ -74,10 +76,11 @@
size="mini"
@click="speakText(option.optionContent)"
:disabled="!isTtsSupported"
class="option-tts-btn"
:class="['option-tts-btn', isSpeaking ? 'speaking' : '']"
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>
</div>
</el-checkbox-group>
@ -134,7 +137,8 @@ export default {
isTtsSupported: false,
synth: null,
currentUtterance: null,
voiceIcon
voiceIcon,
isSpeaking: false
};
},
computed: {
@ -208,45 +212,62 @@ export default {
/** 朗读文本 */
speakText(text) {
if (!this.isTtsSupported || !text || !text.trim()) {
this.$message.warning('浏览器不支持语音播放功能');
return;
}
//
if (this.isSpeaking) {
this.stopSpeaking();
return;
}
this.stopSpeaking();
this.currentUtterance = new SpeechSynthesisUtterance(text.trim());
this.currentUtterance.lang = 'zh-CN';
this.currentUtterance.volume = 1.0;
this.currentUtterance.rate = 1.0;
this.currentUtterance.volume = 1.0; //
this.currentUtterance.rate = 0.9; //
this.currentUtterance.pitch = 1.0;
//
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) {
this.currentUtterance.voice = chineseVoice;
}
let hasStarted = false;
this.currentUtterance.onstart = () => {
hasStarted = true;
this.isSpeaking = true;
};
this.currentUtterance.onend = () => {
this.isSpeaking = false;
};
this.currentUtterance.onerror = (event) => {
console.error('TTS 错误:', event);
if (hasStarted) {
this.isSpeaking = false;
const errorType = event.error || '';
//
const ignoredErrors = ['interrupted', 'canceled'];
if (ignoredErrors.includes(errorType)) {
return;
}
const errorType = event.error || '';
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('语音朗读失败');
}
console.error('TTS 错误:', event);
//
};
try {
//
this.synth.speak(this.currentUtterance);
} catch (error) {
this.isSpeaking = false;
console.error('调用 speak 失败:', error);
this.$message.error('语音朗读失败:无法启动语音合成');
}
},
/** 停止朗读 */
@ -255,6 +276,7 @@ export default {
this.synth.cancel();
}
this.currentUtterance = null;
this.isSpeaking = false;
},
/** 错误信息 */
getErrorMessage(errorType) {
@ -442,10 +464,26 @@ export default {
/** 退出 */
handleExit() {
this.$modal.confirm('确定要退出测评吗?已答题目将会保存。').then(() => {
//
const roles = this.$store.getters.roles || [];
const isStudent = roles.some(role => role === 'student' || role.includes('学员'));
this.$router.push(isStudent ? '/student/tests' : '/psychology/assessment');
this.loading = true;
//
setTimeout(() => {
// 退
pauseAssessment(this.assessmentId).then(() => {
this.loading = false;
this.$modal.msgSuccess("测评进度已保存");
//
const roles = this.$store.getters.roles || [];
const isStudent = roles.some(role => role === 'student' || role.includes('学员'));
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;
}
/* 语音播放时的动画效果 */
.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>

View File

@ -6,15 +6,34 @@
<el-option
v-for="scale in scaleList"
:key="scale.scaleId"
:label="scale.scaleName"
:label="formatScaleLabel(scale)"
: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-select>
</el-form-item>
<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
v-for="user in userList"
v-for="user in userOptions"
:key="user.userId"
:label="user.nickName ? `${user.nickName}${user.userName}` : user.userName"
:value="Number(user.userId)">
@ -45,7 +64,17 @@
<el-table v-loading="loading" :data="permissionList" @selection-change="handleSelectionChange">
<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">
<template slot-scope="scope">
<span v-if="scope.row.hasAllUsers">
@ -66,9 +95,10 @@
<span v-else style="color: #909399;">-</span>
</template>
</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="className" width="150" />
<!-- 隐藏部门角色班级列 -->
<!-- <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="className" width="150" /> -->
<el-table-column label="开始时间" align="center" prop="startTime" width="180">
<template slot-scope="scope">
<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-item label="量表" prop="scaleId">
<el-select v-model="form.scaleId" placeholder="请选择量表" style="width: 100%;" filterable>
<el-option
v-for="scale in scaleList"
:key="scale.scaleId"
:label="scale.scaleName"
:value="scale.scaleId">
</el-option>
</el-select>
<div style="border: 1px solid #DCDFE6; border-radius: 4px; padding: 10px; max-height: 300px; overflow-y: auto;">
<el-input
v-model="scaleSearchKeyword"
placeholder="请输入量表名称搜索"
clearable
size="small"
style="margin-bottom: 10px;"
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 label="用户" prop="userIds">
<el-select
v-model="form.userIds"
placeholder="请选择用户(可多选,留空表示所有用户)"
clearable
filterable
multiple
reserve-keyword
:loading="userSearchLoading"
<el-input
:value="selectedUserDisplay"
placeholder="点击选择用户(留空表示所有用户)"
readonly
style="width: 100%;"
@focus="handleSelectFocus"
@change="handleUserIdsChange">
<el-option
v-for="user in userOptions"
:key="user.userId"
:label="user.nickName ? `${user.nickName}${user.userName}` : user.userName"
:value="user.userId">
</el-option>
</el-select>
@focus="showUserSelectDialog = true">
<el-button slot="append" icon="el-icon-search" @click="showUserSelectDialog = true">选择</el-button>
</el-input>
<div style="margin-top: 5px; color: #909399; font-size: 12px;">
提示可输入姓名或账号搜索支持多选留空表示所有用户
提示留空表示所有用户点击"选择"按钮可按信息编号姓名监区筛选用户
</div>
</el-form-item>
<el-form-item label="开始时间" prop="startTime">
@ -170,6 +220,74 @@
<el-button @click="cancel"> </el-button>
</div>
</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>
</template>
@ -179,6 +297,7 @@ import { listScale } from "@/api/psychology/scale";
import { allocatedUserList } from "@/api/system/role";
import { listRole } from "@/api/system/role";
import { getUser } from "@/api/system/user";
import { listProfile } from "@/api/psychology/profile";
export default {
name: "PsyScalePermission",
@ -200,6 +319,8 @@ export default {
permissionList: [],
//
scaleList: [],
//
scaleSearchKeyword: "",
//
userOptions: [],
//
@ -210,6 +331,26 @@ export default {
title: "",
//
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: {
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() {
this.getList();
this.loadScales();
this.loadUsers();
this.loadPrisonAreaOptions();
},
methods: {
formatScaleLabel(scale) {
if (!scale) {
return ""
}
const prefix = scale.sourceType === 'questionnaire' ? '[问卷]' : '[量表]'
return `${prefix} ${scale.scaleName || ''}`
},
/** 查询权限列表 */
getList() {
this.loading = true;
@ -324,11 +524,16 @@ export default {
},
/** 加载量表列表(显示所有量表,不包含问卷) */
loadScales() {
//
listScale({ includeQuestionnaire: false, pageNum: 1, pageSize: 1000 }).then(response => {
//
this.scaleList = (response.rows || [])
.filter(scale => !scale.sourceType || scale.sourceType === 'scale');
// /
listScale({ includeQuestionnaire: true, pageNum: 1, pageSize: 1000 }).then(response => {
this.scaleList = (response.rows || []).filter(scale => {
if (!scale) return false
//
if (scale.sourceType === 'questionnaire') {
return true
}
return true
})
});
},
/** 加载用户列表(只加载学员角色的用户)- 改为远程搜索模式,不预加载 */
@ -345,6 +550,219 @@ export default {
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) {
if (!this.studentRoleId) {
@ -545,6 +963,9 @@ export default {
_permissionIds: undefined, // ID
_groupKey: undefined // key
};
this.scaleSearchKeyword = "";
this.selectedUserIds = [];
this.cachedFilteredUsers = [];
this.resetForm("form");
},
/** 搜索按钮操作 */
@ -574,6 +995,7 @@ export default {
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.scaleSearchKeyword = "";
// userOptions
if (this.userOptions.length === 0) {
this.searchUsers("");
@ -584,6 +1006,7 @@ export default {
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
this.scaleSearchKeyword = "";
// ID
const permissionIds = row.permissionIds || (row.permissionId ? [row.permissionId] : []);

View File

@ -17,14 +17,6 @@
@keyup.enter.native="handleQuery"
/>
</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-input
v-model="queryParams.prisonArea"
@ -61,14 +53,6 @@
<el-option label="其他" value="其他" />
</el-select>
</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-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
</el-form-item>
@ -126,6 +110,11 @@
<span v-else>{{ scope.row.gender || '-' }}</span>
</template>
</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">
<template slot-scope="scope">
<span>{{ scope.row.nation || '-' }}</span>
@ -136,11 +125,6 @@
<span>{{ scope.row.educationLevel || '-' }}</span>
</template>
</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">
<template slot-scope="scope">
<span>{{ scope.row.crimeName || '-' }}</span>
@ -168,9 +152,9 @@
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="120">
<template slot-scope="scope">
<el-select
v-model="scope.row.status"
size="mini"
<el-select
v-model="scope.row.status"
size="mini"
@change="handleStatusChange(scope.row)"
placeholder="请选择状态"
style="width: 100px;"
@ -258,12 +242,23 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="民族" prop="nation">
<el-input v-model="form.nation" placeholder="请输入民族" />
<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-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-form-item label="文化程度" prop="educationLevel">
<el-select v-model="form.educationLevel" placeholder="请选择文化程度">
@ -279,20 +274,14 @@
</el-select>
</el-form-item>
</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-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-input v-model="form.crimeName" placeholder="请输入罪名" />
</el-form-item>
@ -434,28 +423,42 @@
<!-- 用户档案导入对话框 -->
<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>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<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>
<el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">下载模板</el-link>
</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>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitFileForm"> </el-button>
<el-button @click="upload.open = false"> </el-button>
<el-button type="primary" :loading="upload.isUploading" @click="submitFileForm"> </el-button>
<el-button :disabled="upload.isUploading" @click="upload.open = false"> </el-button>
</div>
</el-dialog>
</div>
</template>
<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 { allocatedUserList } from "@/api/system/role"
import { listRole } from "@/api/system/role"
import { getToken } from "@/utils/auth"
import Treeselect from "@riophae/vue-treeselect"
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
@ -496,6 +499,11 @@ export default {
postOptions: [],
//
roleOptions: [],
// ID
studentRoleId: undefined,
// /
filterPrisonOptions: [],
filterPrisonAreaOptions: [],
//
initPassword: undefined,
//
@ -513,7 +521,9 @@ export default {
educationLevel: undefined,
crimeName: undefined,
userId: undefined,
deptId: undefined
deptId: undefined,
status: undefined,
idCard: undefined
},
//
form: {},
@ -523,11 +533,12 @@ export default {
{ required: true, message: "档案类型不能为空", trigger: "change" }
],
infoNumber: [
{ required: true, message: "信息编号不能为空", trigger: "blur" },
{ pattern: /^\d+$/, message: "信息编号只能输入数字", trigger: "blur" }
],
userName: [
{ required: true, message: "罪犯姓名不能为空", trigger: "blur" },
{ pattern: /^[\u4e00-\u9fa5]+$/, message: "姓名只能输入汉字", trigger: "blur" }
{ pattern: /^[\u4e00-\u9fa5\d]+$/, message: "姓名只能输入汉字和数字", trigger: "blur" }
],
prisonArea: [
{ required: true, message: "监区不能为空", trigger: "blur" }
@ -574,16 +585,27 @@ export default {
//
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() {
this.getList()
this.getDeptTree()
this.getConfigKey("sys.user.initPassword").then(response => {
this.initPassword = response.msg
})
// ID
this.initStudentRoleId()
},
methods: {
/** 查询学员档案列表(后端分页) */
@ -619,6 +641,38 @@ export default {
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() {
this.open = false
@ -635,9 +689,9 @@ export default {
prison: undefined,
prisonArea: undefined,
gender: undefined,
birthday: undefined,
nation: undefined,
educationLevel: undefined,
birthday: undefined,
crimeName: undefined,
sentenceTerm: undefined,
sentenceStartDate: undefined,
@ -654,8 +708,8 @@ export default {
},
//
handleUserNameInput(value) {
//
this.form.userName = value.replace(/[^\u4e00-\u9fa5]/g, '')
//
this.form.userName = value.replace(/[^\u4e00-\u9fa5\d]/g, '')
},
/** 搜索按钮操作 */
handleQuery() {
@ -815,15 +869,15 @@ export default {
this.$modal.msgError("请选择要修改的档案")
return
}
const userId = row.userId
if (!userId) {
this.$modal.msgError("用户ID不能为空")
return
}
this.reset()
// profileId
if (row.profileId) {
getProfile(row.profileId).then(response => {
@ -874,7 +928,6 @@ export default {
gender: (row && row.gender) || undefined,
nation: (row && row.nation) || undefined,
educationLevel: (row && row.educationLevel) || undefined,
birthday: (row && row.birthday) || undefined,
crimeName: (row && row.crimeName) || undefined,
sentenceTerm: (row && row.sentenceTerm) || undefined,
sentenceStartDate: undefined,
@ -896,6 +949,7 @@ export default {
prison: (row && row.prison) || undefined,
prisonArea: (row && row.prisonArea) || undefined,
gender: (row && row.gender) || undefined,
birthday: (row && row.birthday) || undefined,
nation: (row && row.nation) || undefined,
educationLevel: (row && row.educationLevel) || undefined,
crimeName: (row && row.crimeName) || undefined,
@ -948,31 +1002,36 @@ export default {
this.$modal.msgError("请选择要删除的记录")
return
}
const profileIdSet = new Set()
const userIdSet = new Set()
const userIdsWithProfile = new Set() // ID
targets.forEach(item => {
// 使 profileId使 userId
if (item.profileId) {
profileIdSet.add(item.profileId)
// ID
if (item.userId) {
userIdsWithProfile.add(item.userId)
}
} else if (item.userId) {
// profileId userId
userIdSet.add(item.userId)
//
this.checkStudentUsers(targets).then(isAllStudents => {
if (!isAllStudents) {
this.$modal.msgError("只能删除学员用户,系统管理员等非学员用户不能删除")
return
}
})
const profileIds = Array.from(profileIdSet)
// ID
const userIds = Array.from(userIdSet).filter(userId => !userIdsWithProfile.has(userId))
const profileIdSet = new Set()
const userIdSet = new Set()
const userIdsWithProfile = new Set() // ID
targets.forEach(item => {
// 使 profileId使 userId
if (item.profileId) {
profileIdSet.add(item.profileId)
// ID
if (item.userId) {
userIdsWithProfile.add(item.userId)
}
} else if (item.userId) {
// profileId userId
userIdSet.add(item.userId)
}
})
const profileIds = Array.from(profileIdSet)
// ID
const userIds = Array.from(userIdSet).filter(userId => !userIdsWithProfile.has(userId))
if (!profileIds.length && !userIds.length) {
this.$modal.msgError("未找到可删除的档案或用户")
return
}
if (!profileIds.length && !userIds.length) {
this.$modal.msgError("未找到可删除的档案或用户")
return
}
const parts = []
if (profileIds.length) {
@ -981,31 +1040,94 @@ export default {
if (userIds.length) {
parts.push(`用户编号 ${userIds.join(',')}`)
}
const message = `是否确认删除${parts.join(' 和 ')}`
const message = `是否确认删除${parts.join(' 和 ')}`
this.$modal.confirm(message).then(() => {
const tasks = []
//
profileIds.forEach(id => tasks.push(delProfile(id)))
//
//
if (userIds.length) {
// targets profileId
const userIdsWithoutProfile = userIds.filter(userId => {
return !targets.some(item => item.userId === userId && item.profileId)
})
if (userIdsWithoutProfile.length > 0) {
tasks.push(delUserInProfile(userIdsWithoutProfile))
this.$modal.confirm(message).then(() => {
const tasks = []
//
profileIds.forEach(id => tasks.push(delProfile(id)))
//
//
if (userIds.length) {
// targets profileId
const userIdsWithoutProfile = userIds.filter(userId => {
return !targets.some(item => item.userId === userId && item.profileId)
})
if (userIdsWithoutProfile.length > 0) {
tasks.push(delUserInProfile(userIdsWithoutProfile))
}
}
}
return Promise.all(tasks)
}).then(() => {
this.getList()
this.$modal.msgSuccess("删除成功")
return Promise.all(tasks)
}).then(() => {
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(error => {
console.error("删除失败:", error)
const errorMsg = error.msg || error.message || "删除失败"
this.$modal.msgError(errorMsg)
})
}).catch(error => {
console.error("删除失败:", error)
const errorMsg = error.msg || error.message || "删除失败"
this.$modal.msgError(errorMsg)
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)
})
})
},
/** 导出按钮操作 */
@ -1052,7 +1174,78 @@ export default {
this.$modal.msgError('请选择后缀为 "xls"或"xlsx"的文件。')
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.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) {
@ -1062,7 +1255,7 @@ export default {
'2': '外出',
'3': '假释'
}
//
const newStatus = String(row.status || '0')
if (!['0', '1', '2', '3'].includes(newStatus)) {
@ -1071,19 +1264,19 @@ export default {
row.status = '0'
return
}
const statusText = statusMap[newStatus] || '未知'
const oldStatus = row._oldStatus !== undefined ? row._oldStatus : String(row.status || '0')
//
if (newStatus === oldStatus) {
return
}
//
row._oldStatus = oldStatus
row.status = newStatus
//
if (!row.profileId) {
this.$modal.msgError("档案ID不能为空")
@ -1091,16 +1284,16 @@ export default {
delete row._oldStatus
return
}
this.$modal.confirm('确认将状态修改为"' + statusText + '"').then(() => {
//
return getProfile(row.profileId).then(response => {
if (!response || !response.data) {
throw new Error("获取档案信息失败,请确认档案是否存在")
}
const profileData = response.data
//
const updateData = {
profileId: row.profileId,
@ -1109,7 +1302,7 @@ export default {
infoNumber: profileData.infoNumber || row.infoNumber,
userName: profileData.userName || row.userName
}
//
if (!updateData.userId) {
const error = new Error("用户ID不能为空请先完善档案信息")
@ -1117,14 +1310,14 @@ export default {
delete row._oldStatus
throw error
}
if (!updateData.infoNumber) {
const error = new Error("信息编号不能为空,请先完善档案信息")
row.status = oldStatus
delete row._oldStatus
throw error
}
return updateProfile(updateData)
})
}).then(response => {
@ -1135,14 +1328,14 @@ export default {
// Element UI confirm reject
// response message message
const isCancel = !error.response && (!error.message || error.message === '')
if (isCancel) {
//
row.status = oldStatus
delete row._oldStatus
return
}
//
console.error("状态修改失败:", error)
const errorMsg = error.response?.data?.msg || error.message || "状态修改失败"
@ -1187,5 +1380,15 @@ export default {
.app-container {
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>

View File

@ -111,7 +111,7 @@
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</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">
<el-button
size="mini"
@ -334,6 +334,13 @@ export default {
},
created() {
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: {
/** 查询问卷列表 */
@ -395,10 +402,31 @@ export default {
handleUpdate(row) {
this.reset()
const questionnaireId = row.questionnaireId || this.ids[0]
this.handleUpdateFromRoute(questionnaireId)
},
/** 从路由参数或传入的ID打开编辑对话框 */
handleUpdateFromRoute(questionnaireId) {
if (!questionnaireId) {
return
}
this.reset()
getQuestionnaire(questionnaireId).then(response => {
this.form = response.data
this.open = true
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-form :inline="true" size="small">
<el-form-item label="选择用户">
<el-select
v-model="selectedUserId"
style="width: 280px"
<el-autocomplete
v-model="userSearchKeyword"
style="width: 320px"
clearable
filterable
remote
reserve-keyword
:remote-method="searchUsers"
:loading="userOptionsLoading"
placeholder="输入姓名或账号搜索"
@change="handleUserChange"
:fetch-suggestions="searchUsers"
placeholder="输入姓名或信息编号搜索"
value-key="value"
:trigger-on-focus="false"
:debounce="400"
@select="handleUserSelect"
>
<el-option
v-for="item in userOptions"
:key="item.userId"
:label="buildUserLabel(item)"
:value="item.userId"
>
<template slot-scope="{ item }">
<div class="user-option">
<span class="name">{{ item.nickName || item.userName }}</span>
<span class="dept">{{ item.deptName || '未分配单位' }}</span>
<span class="dept">{{ item.infoNumber ? `编号:${item.infoNumber}` : '' }}</span>
</div>
</el-option>
</el-select>
</template>
</el-autocomplete>
</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 icon="el-icon-refresh" @click="resetSelection">重置</el-button>
@ -53,7 +50,7 @@
<el-card class="scale-list" shadow="never" v-loading="loading">
<div slot="header">
<span>量表列表请勾选需要分析的量表</span>
<span>量表/问卷列表请勾选需要分析的量表/问卷</span>
<div class="header-actions">
<el-button
type="primary"
@ -76,7 +73,17 @@
empty-text="暂无可用的测评报告"
>
<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 label="测评时间" width="200" align="center">
<template slot-scope="scope">
@ -107,6 +114,12 @@
<div v-else class="empty-report">报告生成中请稍候...</div>
</div>
<div slot="footer">
<el-button
type="success"
icon="el-icon-printer"
:disabled="!comprehensiveReport"
@click="printReport"
>打印报告</el-button>
<el-button
type="primary"
icon="el-icon-download"
@ -120,8 +133,8 @@
<script>
import { getUserAssessmentSummary, getStudentOptions } from '@/api/psychology/assessment'
import { getProfileByUserId } from '@/api/psychology/profile'
import { getReport } from '@/api/psychology/report'
import { getProfileByUserId, listProfile } from '@/api/psychology/profile'
import { getReport, listReport } from '@/api/psychology/report'
import { parseTime } from '@/utils/ruoyi'
import axios from 'axios'
@ -130,8 +143,9 @@ export default {
data() {
return {
selectedUserId: undefined,
userOptions: [],
userOptionsLoading: false,
userSearchKeyword: '',
userSearchLoading: false,
cachedUserOptions: [],
userProfile: null,
userSummary: null,
reportOptions: [],
@ -140,22 +154,57 @@ export default {
generating: false,
reportDialogVisible: false,
comprehensiveReport: '',
// ========== ==========
OLLAMA_URL: 'http://192.168.0.106:11434/api/generate',
MODEL: 'deepseek-r1:32b'
}
},
created() {
this.searchUsers('')
},
methods: {
searchUsers(query) {
this.userOptionsLoading = true
getStudentOptions({ keyword: query, limit: 20 })
.then((res) => {
this.userOptions = res.data || []
searchUsers(query, cb) {
this.fetchUserOptions(query)
.then((list) => cb(list))
.catch(() => cb([]))
},
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(() => {
this.userOptionsLoading = false
this.userSearchLoading = false
})
},
buildUserLabel(option) {
@ -163,13 +212,112 @@ export default {
return ''
}
const name = option.nickName || option.userName || ''
const info = option.infoNumber ? `(编号:${option.infoNumber}` : ''
const dept = option.deptName ? ` - ${option.deptName}` : ''
return name + dept
return `${name}${info}${dept}`
},
handleUserChange() {
if (!this.selectedUserId) {
this.resetSelection()
handleUserSelect(option) {
if (!option || !option.userId) {
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() {
if (!this.selectedUserId) {
@ -184,7 +332,7 @@ export default {
//
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 reportRows = attempts
.filter((attempt) => attempt && attempt.reportId)
@ -198,10 +346,18 @@ export default {
submitTime: attempt.submitTime || attempt.startTime,
totalScore: attempt.totalScore,
summary: attempt.reportSummary || '',
status: attempt.status
status: attempt.status,
sourceType: 'assessment'
}))
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.$nextTick(() => {
if (this.$refs.reportTable && this.$refs.reportTable.doLayout) {
@ -226,12 +382,44 @@ export default {
},
resetSelection() {
this.selectedUserId = undefined
this.userSearchKeyword = ''
this.userProfile = null
this.userSummary = null
this.reportOptions = []
this.selectedReports = []
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) {
if (!value) return '-'
return parseTime(value)
@ -294,18 +482,20 @@ export default {
continue
}
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) {
reports.push({
scaleName: row.scaleName,
scaleName: row.scaleName || row.reportTitle || '问卷报告',
submitTime: row.submitTime,
totalScore: row.totalScore,
summary: response.data.summary || row.summary || '',
content: response.data.reportContent || ''
content: response.data.reportContent || '',
sourceType
})
}
} catch (error) {
console.warn(`获取量表 ${row.scaleName} 的报告失败:`, error)
console.warn(`获取${row.sourceType === 'questionnaire' ? '问卷' : '量表'} ${row.scaleName} 的报告失败:`, error)
}
}
return reports
@ -357,12 +547,13 @@ export default {
const scaleReportsText = scaleReports
.map((report, index) => {
const typeLabel = report.sourceType === 'questionnaire' ? '问卷' : '量表'
const contentText = report.content
.replace(/<[^>]*>/g, '')
.replace(/&nbsp;/g, ' ')
.substring(0, 500)
return `
量表${index + 1}${report.scaleName}
${typeLabel}${index + 1}${report.scaleName}
测评时间${this.formatDateTime(report.submitTime)}
总分${report.totalScore}
报告摘要${report.summary || '无'}
@ -373,6 +564,7 @@ export default {
return `${SYSTEM_PROMPT}\n\n${userInfoText}\n\n${scaleReportsText}`
},
// OLLAMA API
async callOLLAMA(prompt) {
try {
const { data } = await axios.post(this.OLLAMA_URL, {
@ -397,13 +589,13 @@ export default {
.trim()
if (!response) {
throw new Error('AI分析返回结果为空')
throw new Error('本地AI分析返回结果为空')
}
return response
} catch (error) {
console.error('AI分析失败:', error)
throw new Error('AI分析失败' + (error.message || '未知错误'))
console.error('本地AI分析失败:', error)
throw new Error('本地AI分析失败' + (error.message || '未知错误'))
}
},
formatReport(aiReport, userInfo, scaleReports) {
@ -449,7 +641,8 @@ export default {
</div>
<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>
`
@ -477,13 +670,8 @@ export default {
return html
},
exportReport(format) {
if (!this.comprehensiveReport) {
this.$message.warning('报告内容为空')
return
}
const reportHtml = `
buildReportHtml() {
return `
<html>
<head>
<meta charset="UTF-8" />
@ -505,6 +693,14 @@ export default {
</body>
</html>
`
},
exportReport(format) {
if (!this.comprehensiveReport) {
this.$message.warning('报告内容为空')
return
}
const reportHtml = this.buildReportHtml()
if (format === 'word') {
const blob = new Blob(['\ufeff', reportHtml], { type: 'application/msword' })
@ -518,16 +714,24 @@ export default {
window.URL.revokeObjectURL(link.href)
this.$message.success('导出成功')
} else {
const printWindow = window.open('', '_blank')
if (!printWindow) {
this.$message.error('无法打开打印窗口,请检查浏览器是否阻止了弹窗')
return
}
printWindow.document.write(reportHtml)
printWindow.document.close()
printWindow.focus()
printWindow.print()
this.printReport()
}
},
printReport() {
if (!this.comprehensiveReport) {
this.$message.warning('报告内容为空')
return
}
const reportHtml = this.buildReportHtml()
const printWindow = window.open('', '_blank')
if (!printWindow) {
this.$message.error('无法打开打印窗口,请检查浏览器是否阻止了弹窗')
return
}
printWindow.document.write(reportHtml)
printWindow.document.close()
printWindow.focus()
printWindow.print()
}
}
}

View File

@ -2,20 +2,38 @@
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
<el-form-item label="量表名称" prop="scaleName">
<el-input
<el-select
v-model="queryParams.scaleName"
placeholder="请输入量表名称"
placeholder="请选择或输入量表名称"
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 label="量表编码" prop="scaleCode">
<el-input
<el-select
v-model="queryParams.scaleCode"
placeholder="请输入量表编码"
placeholder="请选择或输入量表编码"
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 label="量表类型" prop="scaleType">
<el-select v-model="queryParams.scaleType" placeholder="量表类型" clearable>
@ -30,7 +48,7 @@
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="状态" clearable>
<el-option
v-for="dict in dict.type.psy_scale_status"
v-for="dict in scaleStatusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
@ -427,6 +445,95 @@
<p style="margin-top: 20px; color: #606266;">正在生成二维码...</p>
</div>
</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>
</template>
@ -454,6 +561,8 @@ export default {
total: 0,
//
scaleList: [],
//
allScaleList: [],
//
title: "",
//
@ -474,6 +583,21 @@ export default {
//
qrcodeOpen: false,
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: {
pageNum: 1,
@ -481,7 +605,8 @@ export default {
scaleName: undefined,
scaleCode: undefined,
scaleType: undefined,
status: undefined
status: undefined,
includeQuestionnaire: true //
},
//
form: {},
@ -513,10 +638,50 @@ export default {
seen.add(dict.value);
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() {
this.getList()
this.getAllScaleList() //
this.getList() //
},
methods: {
/** 获取字典标签 */
@ -525,6 +690,21 @@ export default {
const dict = dictList.find(item => item.value === 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() {
this.loading = true
@ -610,12 +790,14 @@ export default {
updateScale(this.form).then(response => {
this.$modal.msgSuccess("修改成功")
this.open = false
this.getAllScaleList() //
this.getList()
})
} else {
addScale(this.form).then(response => {
this.$modal.msgSuccess("新增成功")
this.open = false
this.getAllScaleList() //
this.getList()
})
}
@ -628,6 +810,7 @@ export default {
this.$modal.confirm('是否确认删除量表编号为"' + scaleIds + '"的数据项?').then(function() {
return delScale(scaleIds)
}).then(() => {
this.getAllScaleList() //
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
@ -779,6 +962,7 @@ export default {
this.$modal.msgSuccess(response.msg || "导入成功")
this.importOpen = false
this.importJsonText = ""
this.getAllScaleList() //
this.getList()
}).catch(error => {
this.$modal.msgError(error.msg || "导入失败")
@ -815,6 +999,7 @@ export default {
this.upload.fileContent = null
this.$refs.upload.clearFiles()
this.upload.isUploading = false
this.getAllScaleList() //
this.getList()
}).catch(error => {
this.$modal.msgError(error.msg || "导入失败")
@ -847,6 +1032,7 @@ export default {
this.upload.fileContent = null
this.$refs.upload.clearFiles()
this.upload.isUploading = false
this.getAllScaleList() //
this.getList()
}).catch(error => {
this.$modal.msgError(error.msg || "导入失败")
@ -916,8 +1102,62 @@ export default {
/** 问卷修改按钮操作 */
handleQuestionnaireUpdate(row) {
const questionnaireId = row.originalId || Math.abs(row.scaleId)
//
this.$router.push({ path: '/psychology/questionnaire', query: { questionnaireId: questionnaireId, action: 'edit' } })
this.resetQuestionnaireForm()
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) {
@ -926,6 +1166,7 @@ export default {
this.$modal.confirm('是否确认删除问卷"' + questionnaireName + '"的数据项?').then(() => {
return delQuestionnaire(questionnaireId)
}).then(() => {
this.getAllScaleList() //
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})

View File

@ -45,7 +45,12 @@
{{ test.description }}
</div>
<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>
</el-card>
@ -63,7 +68,7 @@
* 作用显示所有开放的心理测试题学员可以选择进行测试
*/
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'
export default {
@ -72,7 +77,8 @@ export default {
return {
loading: false,
testList: [],
searchText: ""
searchText: "",
pausedMap: {}
}
},
computed: {
@ -97,16 +103,19 @@ export default {
//
if (to.path === '/student/tests') {
this.loadTestList()
this.loadPausedList()
}
}
},
created() {
//
this.loadTestList()
this.loadPausedList()
},
//
activated() {
this.loadTestList()
this.loadPausedList()
},
methods: {
//
@ -132,6 +141,7 @@ export default {
})
this.loading = false
this.loadPausedList()
}).catch(error => {
console.error("loadTestList, 加载测试题列表失败:", 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() {
// computed
@ -152,6 +189,28 @@ export default {
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') {
const questionnaireId = test.originalId || Math.abs(test.scaleId)
@ -166,6 +225,11 @@ export default {
this.createAssessment(test)
},
//
createNewAssessment(test) {
this.createAssessment(test)
},
//
createAssessment(test) {
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) {
//

View File

@ -160,7 +160,7 @@
</el-form-item>
</el-col>
</el-row>
<!-- 心理档案信息 -->
<el-divider content-position="left">心理档案信息</el-divider>
<el-row>
@ -251,7 +251,7 @@
<!-- 用户导入对话框 -->
<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>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
<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.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() {
const file = this.$refs.upload.uploadFiles
@ -697,4 +689,4 @@ export default {
}
}
}
</script>
</script>

View File

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