修改bug,优化程序。
This commit is contained in:
commit
c014532706
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
{
|
||||
// 问卷类型默认对所有学员开放
|
||||
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);
|
||||
|
|
@ -586,6 +588,24 @@ public class PsyScaleController extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
private Set<Long> resolveRestrictedScaleIds()
|
||||
{
|
||||
try
|
||||
{
|
||||
List<Long> ids = scalePermissionService.selectAllScaleIdsWithPermission();
|
||||
if (ids == null || ids.isEmpty())
|
||||
{
|
||||
return java.util.Collections.emptySet();
|
||||
}
|
||||
return new java.util.HashSet<>(ids);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.warn("解析量表权限限制列表失败: {}", e.getMessage());
|
||||
return java.util.Collections.emptySet();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> normalizeScaleImportMap(Map<String, Object> root)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -91,5 +91,12 @@ public interface PsyScalePermissionMapper
|
|||
* @return 结果
|
||||
*/
|
||||
public int deletePermissionByScaleId(Long scaleId);
|
||||
|
||||
/**
|
||||
* 查询所有已配置权限的量表ID
|
||||
*
|
||||
* @return 量表ID集合
|
||||
*/
|
||||
public List<Long> selectAllScaleIdsWithPermission();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
* 量表权限 服务层实现
|
||||
|
|
@ -28,6 +30,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,11 +493,14 @@ 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();
|
||||
|
||||
try
|
||||
{
|
||||
for (PsyUserProfile profile : profileList)
|
||||
{
|
||||
try
|
||||
|
|
@ -505,6 +513,7 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
|
|||
{
|
||||
failureNum++;
|
||||
failureMsg.append("<br/>").append(failureNum).append("、档案信息编号为空");
|
||||
importProgressManager.recordFailure(progressKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -516,6 +525,7 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
|
|||
this.insertProfile(profile);
|
||||
successNum++;
|
||||
successMsg.append("<br/>").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 导入成功");
|
||||
importProgressManager.recordSuccess(progressKey);
|
||||
}
|
||||
else if (isUpdateSupport)
|
||||
{
|
||||
|
|
@ -526,11 +536,13 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
|
|||
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)
|
||||
|
|
@ -538,21 +550,25 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
|
|||
failureNum++;
|
||||
String msg = "<br/>" + failureNum + "、信息编号 " + profile.getInfoNumber() + " 导入失败:";
|
||||
failureMsg.append(msg).append(e.getMessage());
|
||||
importProgressManager.recordFailure(progressKey);
|
||||
log.error(msg, e);
|
||||
}
|
||||
}
|
||||
|
||||
// 根据成功/失败情况生成结果
|
||||
if (failureNum > 0)
|
||||
{
|
||||
if (successNum == 0)
|
||||
{
|
||||
// 全部失败,保持原有行为:抛出异常
|
||||
// 全部失败
|
||||
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
|
||||
throw new ServiceException(failureMsg.toString());
|
||||
String finalMsg = failureMsg.toString();
|
||||
importProgressManager.finishFailure(progressKey, finalMsg);
|
||||
throw new ServiceException(finalMsg);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 部分成功、部分失败:同时返回成功和失败统计,前端可一次性展示
|
||||
// 部分成功、部分失败:同时返回成功和失败统计
|
||||
StringBuilder resultMsg = new StringBuilder();
|
||||
resultMsg.append("本次导入完成:共 ")
|
||||
.append(successNum + failureNum)
|
||||
|
|
@ -560,20 +576,50 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
|
|||
.append(successNum)
|
||||
.append(" 条,失败 ")
|
||||
.append(failureNum)
|
||||
.append(" 条。成功明细如下:");
|
||||
.append(" 条。");
|
||||
|
||||
if (successNum > 0)
|
||||
{
|
||||
resultMsg.append("<br/>成功明细如下:");
|
||||
resultMsg.append(successMsg);
|
||||
}
|
||||
|
||||
failureMsg.insert(0, "<br/><br/>失败明细如下:");
|
||||
resultMsg.append(failureMsg);
|
||||
return resultMsg.toString();
|
||||
|
||||
String finalMsg = resultMsg.toString();
|
||||
importProgressManager.finishFailure(progressKey, finalMsg);
|
||||
return finalMsg;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
// 没有失败
|
||||
if (successNum > 0)
|
||||
{
|
||||
// 全部成功
|
||||
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
|
||||
return successMsg.toString();
|
||||
String successMessage = "恭喜您,数据已全部导入成功!<br/>" + successMsg.toString();
|
||||
importProgressManager.finishSuccess(progressKey, successMessage);
|
||||
return successMessage;
|
||||
}
|
||||
|
||||
// 理论上不会到这里(既没有成功也没有失败),但为安全起见保留兜底逻辑
|
||||
importProgressManager.finishSuccess(progressKey, "导入完成,但未检测到需要处理的数据。");
|
||||
return "导入完成,但未检测到需要处理的数据。";
|
||||
}
|
||||
catch (RuntimeException ex)
|
||||
{
|
||||
if (!(ex instanceof ServiceException))
|
||||
{
|
||||
importProgressManager.finishFailure(progressKey, ex.getMessage());
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImportProgress getImportProgress(String operName)
|
||||
{
|
||||
String progressKey = importProgressManager.buildProfileKey(operName);
|
||||
return importProgressManager.snapshot(progressKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,13 @@ public interface IPsyScalePermissionService
|
|||
*/
|
||||
public int deletePermissionByScaleId(Long scaleId);
|
||||
|
||||
/**
|
||||
* 查询所有已配置权限的量表ID
|
||||
*
|
||||
* @return 量表ID集合
|
||||
*/
|
||||
public List<Long> selectAllScaleIdsWithPermission();
|
||||
|
||||
/**
|
||||
* 批量分配用户量表权限
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 < 0 then q.questionnaire_name else s.scale_name end as scale_name,
|
||||
case when p.scale_id < 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>
|
||||
|
|
|
|||
|
|
@ -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') >= date_format(#{params.beginTime},'%Y%m%d')
|
||||
</if>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -102,3 +102,11 @@ export function delUserInProfile(userIds) {
|
|||
})
|
||||
}
|
||||
|
||||
// 查询档案导入进度
|
||||
export function getProfileImportProgress() {
|
||||
return request({
|
||||
url: '/psychology/profile/importProgress',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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') &&
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -110,6 +110,11 @@ export default {
|
|||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
'form.profileId'(newVal) {
|
||||
this.refreshPausedListByProfile(newVal)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 检查URL参数中是否有scaleId和profileId/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;
|
||||
// 找到选中的用户档案
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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] : []);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>仅允许导入xls、xlsx格式文件。</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() {
|
||||
|
|
@ -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,7 +1002,12 @@ export default {
|
|||
this.$modal.msgError("请选择要删除的记录")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否都是学员用户(非学员用户不能删除)
|
||||
this.checkStudentUsers(targets).then(isAllStudents => {
|
||||
if (!isAllStudents) {
|
||||
this.$modal.msgError("只能删除学员用户,系统管理员等非学员用户不能删除")
|
||||
return
|
||||
}
|
||||
const profileIdSet = new Set()
|
||||
const userIdSet = new Set()
|
||||
const userIdsWithProfile = new Set() // 记录有档案的用户ID
|
||||
|
|
@ -1007,6 +1066,69 @@ export default {
|
|||
const errorMsg = error.msg || error.message || "删除失败"
|
||||
this.$modal.msgError(errorMsg)
|
||||
})
|
||||
}).catch(error => {
|
||||
console.error("检查学员用户失败:", error)
|
||||
this.$modal.msgError("检查用户角色失败,无法删除")
|
||||
})
|
||||
},
|
||||
/** 初始化学员角色ID */
|
||||
initStudentRoleId() {
|
||||
listRole({}).then(response => {
|
||||
const roles = response.rows || response.data || []
|
||||
if (!Array.isArray(roles) || roles.length === 0) {
|
||||
return
|
||||
}
|
||||
let candidate = roles.find(r => r && r.roleKey && r.roleKey.toLowerCase() === 'student')
|
||||
if (!candidate) {
|
||||
candidate = roles.find(r => r && r.roleName && r.roleName.indexOf('学员') !== -1)
|
||||
}
|
||||
if (!candidate) {
|
||||
candidate = roles.find(r => r && r.roleId === 101)
|
||||
}
|
||||
if (candidate && candidate.roleId) {
|
||||
this.studentRoleId = candidate.roleId
|
||||
}
|
||||
}).catch(() => {
|
||||
// 角色获取失败时,不中断页面其它功能
|
||||
})
|
||||
},
|
||||
/** 检查用户是否都是学员用户 */
|
||||
checkStudentUsers(targets) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!targets || targets.length === 0) {
|
||||
resolve(false)
|
||||
return
|
||||
}
|
||||
// 如果学员角色ID尚未初始化,则不允许删除,给出提示
|
||||
if (!this.studentRoleId) {
|
||||
this.$modal.msgError('未正确配置学员角色,暂不允许删除用户,请联系管理员')
|
||||
resolve(false)
|
||||
return
|
||||
}
|
||||
// 获取所有学员用户ID列表
|
||||
allocatedUserList({
|
||||
roleId: this.studentRoleId,
|
||||
status: '0',
|
||||
pageNum: 1,
|
||||
pageSize: 10000
|
||||
}).then(response => {
|
||||
const studentUserIds = new Set()
|
||||
const studentUsers = response.rows || []
|
||||
studentUsers.forEach(user => {
|
||||
if (user.userId) {
|
||||
studentUserIds.add(user.userId)
|
||||
}
|
||||
})
|
||||
// 检查所有目标用户是否都是学员用户
|
||||
const allAreStudents = targets.every(item => {
|
||||
return item.userId && studentUserIds.has(item.userId)
|
||||
})
|
||||
resolve(allAreStudents)
|
||||
}).catch(error => {
|
||||
console.error("获取学员用户列表失败:", error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
/** 导出按钮操作 */
|
||||
handleExport() {
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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("获取问卷信息失败")
|
||||
})
|
||||
},
|
||||
/** 提交按钮 */
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in userOptions"
|
||||
:key="item.userId"
|
||||
:label="buildUserLabel(item)"
|
||||
:value="item.userId"
|
||||
:fetch-suggestions="searchUsers"
|
||||
placeholder="输入姓名或信息编号搜索"
|
||||
value-key="value"
|
||||
:trigger-on-focus="false"
|
||||
:debounce="400"
|
||||
@select="handleUserSelect"
|
||||
>
|
||||
<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(/ /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,6 +714,15 @@ export default {
|
|||
window.URL.revokeObjectURL(link.href)
|
||||
this.$message.success('导出成功')
|
||||
} else {
|
||||
this.printReport()
|
||||
}
|
||||
},
|
||||
printReport() {
|
||||
if (!this.comprehensiveReport) {
|
||||
this.$message.warning('报告内容为空')
|
||||
return
|
||||
}
|
||||
const reportHtml = this.buildReportHtml()
|
||||
const printWindow = window.open('', '_blank')
|
||||
if (!printWindow) {
|
||||
this.$message.error('无法打开打印窗口,请检查浏览器是否阻止了弹窗')
|
||||
|
|
@ -529,7 +734,6 @@ export default {
|
|||
printWindow.print()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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(() => {})
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
// 这里可以根据实际需求返回类型名称
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user