diff --git a/ry-xinli-system/src/main/java/com/ddnai/system/mapper/SysUserMapper.java b/ry-xinli-system/src/main/java/com/ddnai/system/mapper/SysUserMapper.java index 1e432a70..744a52ce 100644 --- a/ry-xinli-system/src/main/java/com/ddnai/system/mapper/SysUserMapper.java +++ b/ry-xinli-system/src/main/java/com/ddnai/system/mapper/SysUserMapper.java @@ -153,5 +153,21 @@ public interface SysUserMapper * @param nickName 昵称 */ public void updateUserName(@Param("userId") Long userId, @Param("userName") String userName, @Param("nickName") String nickName); + + /** + * 批量新增用户信息 + * + * @param userList 用户信息列表 + * @return 结果 + */ + public int batchInsertUser(@Param("list") List userList); + + /** + * 批量查询用户名是否存在 + * + * @param userNames 用户名列表 + * @return 已存在的用户列表 + */ + public List selectUsersByUserNames(@Param("userNames") List userNames); } diff --git a/ry-xinli-system/src/main/java/com/ddnai/system/mapper/psychology/PsyUserProfileMapper.java b/ry-xinli-system/src/main/java/com/ddnai/system/mapper/psychology/PsyUserProfileMapper.java index 51e9d3b5..f8a31c04 100644 --- a/ry-xinli-system/src/main/java/com/ddnai/system/mapper/psychology/PsyUserProfileMapper.java +++ b/ry-xinli-system/src/main/java/com/ddnai/system/mapper/psychology/PsyUserProfileMapper.java @@ -81,5 +81,29 @@ public interface PsyUserProfileMapper * @return 结果 */ public int deleteProfileByIds(Long[] profileIds); + + /** + * 批量查询档案(根据信息编号列表) + * + * @param infoNumbers 信息编号列表 + * @return 档案集合 + */ + public List selectProfilesByInfoNumbers(List infoNumbers); + + /** + * 批量插入档案 + * + * @param profileList 档案列表 + * @return 结果 + */ + public int batchInsertProfiles(List profileList); + + /** + * 批量更新档案 + * + * @param profileList 档案列表 + * @return 结果 + */ + public int batchUpdateProfiles(List profileList); } diff --git a/ry-xinli-system/src/main/java/com/ddnai/system/service/impl/SysUserServiceImpl.java b/ry-xinli-system/src/main/java/com/ddnai/system/service/impl/SysUserServiceImpl.java index f6f9aa52..851c30ef 100644 --- a/ry-xinli-system/src/main/java/com/ddnai/system/service/impl/SysUserServiceImpl.java +++ b/ry-xinli-system/src/main/java/com/ddnai/system/service/impl/SysUserServiceImpl.java @@ -3,6 +3,7 @@ package com.ddnai.system.service.impl; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import javax.validation.Validator; import org.slf4j.Logger; @@ -524,7 +525,7 @@ public class SysUserServiceImpl implements ISysUserService } /** - * 导入用户数据 + * 导入用户数据(优化版 - 批量处理) * * @param userList 用户数据列表 * @param isUpdateSupport 是否更新支持,如果已存在,则进行更新数据 @@ -538,10 +539,20 @@ public class SysUserServiceImpl implements ISysUserService { throw new ServiceException("导入用户数据不能为空!"); } + int successNum = 0; int failureNum = 0; StringBuilder successMsg = new StringBuilder(); StringBuilder failureMsg = new StringBuilder(); + + // 获取默认密码(只查一次) + String defaultPassword = configService.selectConfigByKey("sys.user.initPassword"); + String encryptedPassword = SecurityUtils.encryptPassword(defaultPassword); + + // 第一步:数据预处理和验证 + List validUsers = new ArrayList<>(); + List userNameList = new ArrayList<>(); + for (SysUser user : userList) { try @@ -549,7 +560,7 @@ public class SysUserServiceImpl implements ISysUserService // 如果有信息编号,使用信息编号作为登录账号 if (StringUtils.isNotEmpty(user.getInfoNumber())) { - user.setUserName(user.getInfoNumber()); // 登录账号 = 信息编号 + user.setUserName(user.getInfoNumber()); } // 如果没有用户名,跳过 @@ -560,54 +571,136 @@ public class SysUserServiceImpl implements ISysUserService continue; } - // 验证是否存在这个用户 - SysUser u = userMapper.selectUserByUserName(user.getUserName()); - if (StringUtils.isNull(u)) - { - BeanValidators.validateWithException(validator, user); - deptService.checkDeptDataScope(user.getDeptId()); - String password = configService.selectConfigByKey("sys.user.initPassword"); - user.setPassword(SecurityUtils.encryptPassword(password)); - user.setCreateBy(operName); - userMapper.insertUser(user); - successNum++; - successMsg.append("
" + successNum + "、账号 " + user.getUserName() + " 导入成功"); - } - else if (isUpdateSupport) - { - BeanValidators.validateWithException(validator, user); - checkUserAllowed(u); - checkUserDataScope(u.getUserId()); - deptService.checkDeptDataScope(user.getDeptId()); - user.setUserId(u.getUserId()); - user.setDeptId(u.getDeptId()); - user.setUpdateBy(operName); - userMapper.updateUser(user); - successNum++; - successMsg.append("
" + successNum + "、账号 " + user.getUserName() + " 更新成功"); - } - else - { - failureNum++; - failureMsg.append("
" + failureNum + "、账号 " + user.getUserName() + " 已存在"); - } + // 基本验证 + BeanValidators.validateWithException(validator, user); + + // 设置默认密码 + user.setPassword(encryptedPassword); + user.setCreateBy(operName); + + validUsers.add(user); + userNameList.add(user.getUserName()); } catch (Exception e) { failureNum++; - String msg = "
" + failureNum + "、账号 " + user.getUserName() + " 导入失败:"; + String msg = "
" + failureNum + "、账号 " + user.getUserName() + " 验证失败:"; failureMsg.append(msg + e.getMessage()); log.error(msg, e); } } + + if (validUsers.isEmpty()) + { + throw new ServiceException("没有有效的用户数据可以导入!"); + } + + // 第二步:批量查询已存在的用户(只查一次数据库) + List existingUsers = userMapper.selectUsersByUserNames(userNameList); + Map existingUserMap = existingUsers.stream() + .collect(Collectors.toMap(SysUser::getUserName, u -> u)); + + // 第三步:分类处理 + List usersToInsert = new ArrayList<>(); + List usersToUpdate = new ArrayList<>(); + + for (SysUser user : validUsers) + { + SysUser existingUser = existingUserMap.get(user.getUserName()); + if (existingUser == null) + { + // 新用户 + usersToInsert.add(user); + } + else if (isUpdateSupport) + { + // 更新用户 + user.setUserId(existingUser.getUserId()); + user.setDeptId(existingUser.getDeptId()); + user.setUpdateBy(operName); + usersToUpdate.add(user); + } + else + { + // 用户已存在且不更新 + failureNum++; + failureMsg.append("
").append(failureNum).append("、账号 ").append(user.getUserName()).append(" 已存在"); + } + } + + // 第四步:批量插入新用户(每500条一批) + if (!usersToInsert.isEmpty()) + { + int batchSize = 500; + for (int i = 0; i < usersToInsert.size(); i += batchSize) + { + int end = Math.min(i + batchSize, usersToInsert.size()); + List batch = usersToInsert.subList(i, end); + try + { + int inserted = userMapper.batchInsertUser(batch); + successNum += inserted; + for (SysUser user : batch) + { + successMsg.append("
").append(successNum).append("、账号 ").append(user.getUserName()).append(" 导入成功"); + } + } + catch (Exception e) + { + // 如果批量插入失败,降级为逐条插入 + log.warn("批量插入失败,降级为逐条插入", e); + for (SysUser user : batch) + { + try + { + userMapper.insertUser(user); + successNum++; + successMsg.append("
").append(successNum).append("、账号 ").append(user.getUserName()).append(" 导入成功"); + } + catch (Exception ex) + { + failureNum++; + failureMsg.append("
").append(failureNum).append("、账号 ").append(user.getUserName()).append(" 导入失败:").append(ex.getMessage()); + log.error("导入用户失败", ex); + } + } + } + } + } + + // 第五步:批量更新用户 + if (!usersToUpdate.isEmpty()) + { + for (SysUser user : usersToUpdate) + { + try + { + userMapper.updateUser(user); + successNum++; + successMsg.append("
").append(successNum).append("、账号 ").append(user.getUserName()).append(" 更新成功"); + } + catch (Exception e) + { + failureNum++; + failureMsg.append("
").append(failureNum).append("、账号 ").append(user.getUserName()).append(" 更新失败:").append(e.getMessage()); + log.error("更新用户失败", e); + } + } + } + + // 返回结果 if (failureNum > 0) { - failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:"); - throw new ServiceException(failureMsg.toString()); + failureMsg.insert(0, "导入完成!成功 " + successNum + " 条,失败 " + failureNum + " 条,错误如下:"); + if (successNum == 0) + { + throw new ServiceException(failureMsg.toString()); + } + return failureMsg.toString(); } else { - successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:"); + successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条"); } return successMsg.toString(); } diff --git a/ry-xinli-system/src/main/java/com/ddnai/system/service/impl/psychology/PsyUserProfileServiceImpl.java b/ry-xinli-system/src/main/java/com/ddnai/system/service/impl/psychology/PsyUserProfileServiceImpl.java index e3f147e9..601521e0 100644 --- a/ry-xinli-system/src/main/java/com/ddnai/system/service/impl/psychology/PsyUserProfileServiceImpl.java +++ b/ry-xinli-system/src/main/java/com/ddnai/system/service/impl/psychology/PsyUserProfileServiceImpl.java @@ -1,6 +1,7 @@ package com.ddnai.system.service.impl.psychology; import java.sql.SQLIntegrityConstraintViolationException; +import java.util.ArrayList; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -501,57 +502,161 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService StringBuilder failureMsg = new StringBuilder(); try { + // ========== 批量处理优化 ========== + // 1. 数据预处理和验证 + List validProfiles = new ArrayList<>(); + List infoNumbers = new ArrayList<>(); + for (PsyUserProfile profile : profileList) { - try - { - // 设置创建者 - profile.setCreateBy(operName); - - // 验证信息编号 - if (StringUtils.isEmpty(profile.getInfoNumber())) - { - failureNum++; - failureMsg.append("
").append(failureNum).append("、档案信息编号为空"); - importProgressManager.recordFailure(progressKey); - continue; - } - - // 根据信息编号查询是否存在 - PsyUserProfile existProfile = profileMapper.selectProfileByInfoNumber(profile.getInfoNumber()); - if (StringUtils.isNull(existProfile)) - { - // 新增档案 - this.insertProfile(profile); - successNum++; - successMsg.append("
").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 导入成功"); - importProgressManager.recordSuccess(progressKey); - } - else if (isUpdateSupport) - { - // 更新档案 - profile.setProfileId(existProfile.getProfileId()); - profile.setUserId(existProfile.getUserId()); - profile.setUpdateBy(operName); - this.updateProfile(profile); - successNum++; - successMsg.append("
").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 更新成功"); - importProgressManager.recordSuccess(progressKey); - } - else - { - failureNum++; - failureMsg.append("
").append(failureNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 已存在"); - importProgressManager.recordFailure(progressKey); - } - } - catch (Exception e) + // 设置创建者 + profile.setCreateBy(operName); + + // 验证信息编号 + if (StringUtils.isEmpty(profile.getInfoNumber())) { failureNum++; - String msg = "
" + failureNum + "、信息编号 " + profile.getInfoNumber() + " 导入失败:"; - failureMsg.append(msg).append(e.getMessage()); + failureMsg.append("
").append(failureNum).append("、档案信息编号为空"); importProgressManager.recordFailure(progressKey); - log.error(msg, e); + continue; + } + + validProfiles.add(profile); + infoNumbers.add(profile.getInfoNumber()); + } + + if (validProfiles.isEmpty()) + { + String msg = "没有有效的用户档案数据!"; + importProgressManager.finishFailure(progressKey, msg); + throw new ServiceException(msg); + } + + // 2. 批量查询已存在的档案(一次查询) + List existingProfiles = profileMapper.selectProfilesByInfoNumbers(infoNumbers); + java.util.Map existingMap = new java.util.HashMap<>(); + for (PsyUserProfile existing : existingProfiles) + { + existingMap.put(existing.getInfoNumber(), existing); + } + + // 3. 分类处理:新增列表和更新列表 + List toInsertList = new ArrayList<>(); + List toUpdateList = new ArrayList<>(); + + for (PsyUserProfile profile : validProfiles) + { + PsyUserProfile existProfile = existingMap.get(profile.getInfoNumber()); + + if (existProfile == null) + { + // 不存在,加入新增列表 + toInsertList.add(profile); + } + else if (isUpdateSupport) + { + // 存在且支持更新,加入更新列表 + profile.setProfileId(existProfile.getProfileId()); + profile.setUserId(existProfile.getUserId()); + profile.setUpdateBy(operName); + toUpdateList.add(profile); + } + else + { + // 存在但不支持更新,记录失败 + failureNum++; + failureMsg.append("
").append(failureNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 已存在"); + importProgressManager.recordFailure(progressKey); + } + } + + // 4. 批量插入(分批处理,每批500条) + if (!toInsertList.isEmpty()) + { + int batchSize = 500; + for (int i = 0; i < toInsertList.size(); i += batchSize) + { + int end = Math.min(i + batchSize, toInsertList.size()); + List batch = toInsertList.subList(i, end); + + try + { + int insertCount = profileMapper.batchInsertProfiles(batch); + for (PsyUserProfile profile : batch) + { + successNum++; + successMsg.append("
").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 导入成功"); + importProgressManager.recordSuccess(progressKey); + } + } + catch (Exception e) + { + // 批量插入失败,尝试逐条插入 + log.warn("批量插入失败,尝试逐条插入: " + e.getMessage()); + for (PsyUserProfile profile : batch) + { + try + { + this.insertProfile(profile); + successNum++; + successMsg.append("
").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 导入成功"); + importProgressManager.recordSuccess(progressKey); + } + catch (Exception ex) + { + failureNum++; + String msg = "
" + failureNum + "、信息编号 " + profile.getInfoNumber() + " 导入失败:"; + failureMsg.append(msg).append(ex.getMessage()); + importProgressManager.recordFailure(progressKey); + log.error(msg, ex); + } + } + } + } + } + + // 5. 批量更新(分批处理,每批500条) + if (!toUpdateList.isEmpty()) + { + int batchSize = 500; + for (int i = 0; i < toUpdateList.size(); i += batchSize) + { + int end = Math.min(i + batchSize, toUpdateList.size()); + List batch = toUpdateList.subList(i, end); + + try + { + int updateCount = profileMapper.batchUpdateProfiles(batch); + for (PsyUserProfile profile : batch) + { + successNum++; + successMsg.append("
").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 更新成功"); + importProgressManager.recordSuccess(progressKey); + } + } + catch (Exception e) + { + // 批量更新失败,尝试逐条更新 + log.warn("批量更新失败,尝试逐条更新: " + e.getMessage()); + for (PsyUserProfile profile : batch) + { + try + { + this.updateProfile(profile); + successNum++; + successMsg.append("
").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 更新成功"); + importProgressManager.recordSuccess(progressKey); + } + catch (Exception ex) + { + failureNum++; + String msg = "
" + failureNum + "、信息编号 " + profile.getInfoNumber() + " 更新失败:"; + failureMsg.append(msg).append(ex.getMessage()); + importProgressManager.recordFailure(progressKey); + log.error(msg, ex); + } + } + } } } diff --git a/ry-xinli-system/src/main/resources/mapper/system/SysUserMapper.xml b/ry-xinli-system/src/main/resources/mapper/system/SysUserMapper.xml index b70f33b4..0763b686 100644 --- a/ry-xinli-system/src/main/resources/mapper/system/SysUserMapper.xml +++ b/ry-xinli-system/src/main/resources/mapper/system/SysUserMapper.xml @@ -232,5 +232,30 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{userId} + + + + insert into sys_user( + dept_id, user_name, nick_name, email, phonenumber, sex, avatar, + password, status, del_flag, create_by, create_time, remark, info_number + ) values + + ( + #{item.deptId}, #{item.userName}, #{item.nickName}, #{item.email}, + #{item.phonenumber}, #{item.sex}, #{item.avatar}, #{item.password}, + #{item.status}, '0', #{item.createBy}, sysdate(), #{item.remark}, #{item.infoNumber} + ) + + + + + diff --git a/ry-xinli-system/src/main/resources/mapper/system/psychology/PsyUserProfileMapper.xml b/ry-xinli-system/src/main/resources/mapper/system/psychology/PsyUserProfileMapper.xml index 25930299..ded621d4 100644 --- a/ry-xinli-system/src/main/resources/mapper/system/psychology/PsyUserProfileMapper.xml +++ b/ry-xinli-system/src/main/resources/mapper/system/psychology/PsyUserProfileMapper.xml @@ -275,5 +275,64 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" order by u.create_time desc + + + + + + insert into psy_user_profile( + user_id, profile_type, profile_data, avatar, id_card, birthday, + prison, prison_area, gender, nation, education_level, crime_name, + sentence_term, sentence_start_date, sentence_end_date, entry_date, + info_number, create_by, create_time, remark + ) + values + + ( + #{item.userId}, #{item.profileType}, #{item.profileData}, #{item.avatar}, + #{item.idCard}, #{item.birthday}, #{item.prison}, #{item.prisonArea}, + #{item.gender}, #{item.nation}, #{item.educationLevel}, #{item.crimeName}, + #{item.sentenceTerm}, #{item.sentenceStartDate}, #{item.sentenceEndDate}, + #{item.entryDate}, #{item.infoNumber}, #{item.createBy}, sysdate(), #{item.remark} + ) + + + + + + + update psy_user_profile + + profile_type = #{item.profileType}, + profile_data = #{item.profileData}, + avatar = #{item.avatar}, + id_card = #{item.idCard}, + birthday = #{item.birthday}, + prison = #{item.prison}, + prison_area = #{item.prisonArea}, + gender = #{item.gender}, + nation = #{item.nation}, + education_level = #{item.educationLevel}, + crime_name = #{item.crimeName}, + sentence_term = #{item.sentenceTerm}, + sentence_start_date = #{item.sentenceStartDate}, + sentence_end_date = #{item.sentenceEndDate}, + entry_date = #{item.entryDate}, + update_by = #{item.updateBy}, + update_time = sysdate(), + remark = #{item.remark}, + + where profile_id = #{item.profileId} + + + diff --git a/xinli-ui/src/views/psychology/assessment/taking.vue b/xinli-ui/src/views/psychology/assessment/taking.vue index f2e3fc90..d690ccba 100644 --- a/xinli-ui/src/views/psychology/assessment/taking.vue +++ b/xinli-ui/src/views/psychology/assessment/taking.vue @@ -33,17 +33,31 @@
第 {{ currentIndex + 1 }} 题
{{ currentItem.itemContent }}
- - - +
+ + + 朗读全部 + + + + +
@@ -226,7 +240,7 @@ export default { this.currentUtterance = new SpeechSynthesisUtterance(text.trim()); this.currentUtterance.lang = 'zh-CN'; this.currentUtterance.volume = 1.0; // 最大音量 - this.currentUtterance.rate = 0.9; // 稍慢一点,更清晰 + this.currentUtterance.rate = 1.2; // 正常语速,稍快 this.currentUtterance.pitch = 1.0; // 获取可用的语音列表 @@ -278,6 +292,33 @@ export default { this.currentUtterance = null; this.isSpeaking = false; }, + /** 朗读当前题目和所有选项 */ + speakCurrentQuestion() { + if (!this.isTtsSupported || !this.currentItem) { + this.$message.warning('浏览器不支持语音播放功能'); + return; + } + + // 如果正在播放,则停止 + if (this.isSpeaking) { + this.stopSpeaking(); + return; + } + + // 构建完整文本:题目 + 所有选项 + let fullText = this.currentItem.itemContent.trim(); + + if (this.currentOptions && this.currentOptions.length > 0) { + fullText += '。选项:'; + this.currentOptions.forEach((option, index) => { + const optionCode = option.optionCode || String.fromCharCode(65 + index); // A, B, C... + fullText += `${optionCode}、${option.optionContent}。`; + }); + } + + // 调用朗读 + this.speakText(fullText); + }, /** 错误信息 */ getErrorMessage(errorType) { const errorMap = { @@ -767,15 +808,32 @@ export default { flex: 1; } -.tts-btn { +.tts-buttons { + display: flex; + gap: 5px; flex-shrink: 0; - padding: 5px 10px; - color: #409EFF; } -.tts-btn:disabled { +.tts-btn-all, .tts-btn { + flex-shrink: 0; + padding: 8px 12px; + border-radius: 4px; + transition: all 0.3s; +} + +.tts-btn-all:hover:not(:disabled), .tts-btn:hover:not(:disabled) { + background-color: #f5f7fa; + transform: scale(1.05); +} + +.tts-btn-all.speaking, .tts-btn.speaking { + animation: pulse 1.5s ease-in-out infinite; +} + +.tts-btn:disabled, .tts-btn-all:disabled { color: #c0c4cc; cursor: not-allowed; + opacity: 0.5; } .tts-icon { diff --git a/xinli-ui/src/views/psychology/permission/index.vue b/xinli-ui/src/views/psychology/permission/index.vue index 0740bdb5..afb7c262 100644 --- a/xinli-ui/src/views/psychology/permission/index.vue +++ b/xinli-ui/src/views/psychology/permission/index.vue @@ -297,7 +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"; +import { listStudentProfile } from "@/api/psychology/profile"; export default { name: "PsyScalePermission", @@ -345,11 +345,11 @@ export default { // 用户查询参数 userQueryParams: { pageNum: 1, - pageSize: 8, + pageSize: 10, infoNumber: undefined, userName: undefined, prisonArea: undefined, - status: '0' + status: undefined }, // 查询参数 queryParams: { @@ -552,7 +552,8 @@ export default { }, /** 加载监区下拉选项 */ loadPrisonAreaOptions() { - listProfile({ pageNum: 1, pageSize: 10000 }).then(response => { + // 使用学员档案接口,与用户档案页面保持一致 + listStudentProfile({ pageNum: 1, pageSize: 10000 }).then(response => { const rows = response.rows || []; const areaSet = new Set(); rows.forEach(profile => { @@ -595,7 +596,7 @@ export default { prisonArea: this.userQueryParams.prisonArea, status: this.userQueryParams.status }; - return listProfile(query).then(response => { + return listStudentProfile(query).then(response => { this.userSelectList = (response.rows || []).map(profile => this.transformProfileToSelectableUser(profile)); this.userSelectTotal = response.total || 0; this.userSelectLoading = false; @@ -619,11 +620,11 @@ export default { this.resetForm("userQueryForm"); this.userQueryParams = { pageNum: 1, - pageSize: 8, + pageSize: 10, infoNumber: undefined, userName: undefined, prisonArea: undefined, - status: '0' + status: undefined }; this.handleUserQuery(); }, @@ -646,14 +647,17 @@ export default { // 取消全选时,清除当前页的选择 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(); + // 只选择当前页的用户 + const currentPageIds = this.userSelectList.map(item => item.userId); + currentPageIds.forEach(userId => { + if (!this.selectedUserIds.includes(userId)) { + this.selectedUserIds.push(userId); + } + }); } }, async fetchAllUsersUnderCurrentFilter() { @@ -668,7 +672,7 @@ export default { status: this.userQueryParams.status }; while (true) { - const response = await listProfile({ + const response = await listStudentProfile({ ...baseParams, pageNum, pageSize diff --git a/xinli-ui/src/views/psychology/questionnaire/taking.vue b/xinli-ui/src/views/psychology/questionnaire/taking.vue index ba24c778..fff8df52 100644 --- a/xinli-ui/src/views/psychology/questionnaire/taking.vue +++ b/xinli-ui/src/views/psychology/questionnaire/taking.vue @@ -21,16 +21,31 @@
第 {{ currentIndex + 1 }} 题
{{ currentItem.itemContent }}
- - 朗读题目 - +
+ + + 朗读全部 + + + + +
分值:{{ currentItem.score }}分 @@ -236,6 +251,7 @@ export default { isTtsSupported: false, synth: null, currentUtterance: null, + isSpeaking: false, voiceIcon }; }, @@ -320,8 +336,8 @@ export default { // 设置语音参数 this.currentUtterance.lang = 'zh-CN'; - this.currentUtterance.volume = 1.0; - this.currentUtterance.rate = 1.0; + this.currentUtterance.volume = 1.0; // 最大音量 + this.currentUtterance.rate = 1.2; // 正常语速,稍快 this.currentUtterance.pitch = 1.0; // 选择中文语音(如果可用) @@ -339,14 +355,17 @@ export default { this.currentUtterance.onstart = () => { hasStarted = true; + this.isSpeaking = true; console.log('TTS 开始朗读'); }; this.currentUtterance.onend = () => { + this.isSpeaking = false; console.log('TTS 朗读完成'); }; this.currentUtterance.onerror = (event) => { + this.isSpeaking = false; console.error('TTS 错误:', event); // 如果已经成功开始朗读,忽略后续的错误(可能是非致命性错误) @@ -385,6 +404,34 @@ export default { this.synth.cancel(); } this.currentUtterance = null; + this.isSpeaking = false; + }, + /** 朗读当前题目和所有选项 */ + speakCurrentQuestion() { + if (!this.isTtsSupported || !this.currentItem) { + this.$message.warning('浏览器不支持语音播放功能'); + return; + } + + // 如果正在播放,则停止 + if (this.isSpeaking) { + this.stopSpeaking(); + return; + } + + // 构建完整文本:题目 + 所有选项 + let fullText = this.currentItem.itemContent.trim(); + + if (this.currentOptions && this.currentOptions.length > 0) { + fullText += '。选项:'; + this.currentOptions.forEach((option, index) => { + const optionCode = option.optionCode || String.fromCharCode(65 + index); // A, B, C... + fullText += `${optionCode}、${option.optionContent}。`; + }); + } + + // 调用朗读 + this.speakText(fullText); }, /** 获取错误信息 */ getErrorMessage(errorType) { @@ -787,20 +834,41 @@ export default { flex: 1; } -.tts-btn { +.tts-buttons { + display: flex; + gap: 5px; flex-shrink: 0; - padding: 5px 10px; - font-size: 16px; - color: #409EFF; } -.tts-btn:hover { - color: #66b1ff; +.tts-btn-all, .tts-btn { + flex-shrink: 0; + padding: 8px 12px; + border-radius: 4px; + transition: all 0.3s; } -.tts-btn:disabled { +.tts-btn-all:hover:not(:disabled), .tts-btn:hover:not(:disabled) { + background-color: #f5f7fa; + transform: scale(1.05); +} + +.tts-btn-all.speaking, .tts-btn.speaking { + animation: pulse 1.5s ease-in-out infinite; +} + +.tts-btn:disabled, .tts-btn-all:disabled { color: #c0c4cc; cursor: not-allowed; + opacity: 0.5; +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.6; + } } .tts-icon { diff --git a/xinli-ui/src/views/psychology/report/comprehensive.vue b/xinli-ui/src/views/psychology/report/comprehensive.vue index 635152a5..2134cced 100644 --- a/xinli-ui/src/views/psychology/report/comprehensive.vue +++ b/xinli-ui/src/views/psychology/report/comprehensive.vue @@ -154,9 +154,11 @@ export default { generating: false, reportDialogVisible: false, comprehensiveReport: '', - // ========== 本地大模型配置 ========== - OLLAMA_URL: 'http://192.168.0.106:11434/api/generate', - MODEL: 'deepseek-r1:32b' + // ========== 本地大模型API配置 ========== + // 注:使用本地大模型,如Ollama等 + API_URL: 'http://localhost:11434/v1/chat/completions', + API_KEY: '', // 本地模型不需要API Key + MODEL: 'qwen2.5:7b' // 根据实际使用的本地模型修改 } }, created() { @@ -564,20 +566,25 @@ ${typeLabel}${index + 1}:${report.scaleName} return `${SYSTEM_PROMPT}\n\n${userInfoText}\n\n${scaleReportsText}` }, - // 本地 OLLAMA API 调用方法 + // Kimi API 调用方法 async callOLLAMA(prompt) { try { - const { data } = await axios.post(this.OLLAMA_URL, { + const { data } = await axios.post(this.API_URL, { model: this.MODEL, - prompt: prompt, + messages: [ + { role: 'user', content: prompt } + ], temperature: 0.2, - num_predict: 2000, - stream: false + max_tokens: 2000 }, { + headers: { + 'Authorization': `Bearer ${this.API_KEY}`, + 'Content-Type': 'application/json' + }, timeout: 120000 }) - let response = data?.response ?? '' + let response = data?.choices?.[0]?.message?.content ?? '' // 清理响应内容 response = response diff --git a/xinli-ui/src/views/psychology/report/detail.vue b/xinli-ui/src/views/psychology/report/detail.vue index ff25129e..2e48ad68 100644 --- a/xinli-ui/src/views/psychology/report/detail.vue +++ b/xinli-ui/src/views/psychology/report/detail.vue @@ -403,9 +403,10 @@ export default { this.aiError = ''; this.aiResult = ''; - // Ollama API配置 - const OLLAMA_URL = 'http://192.168.0.106:11434/api/generate'; - const MODEL = 'deepseek-r1:32b'; + // 本地大模型API配置(如Ollama) + const API_URL = 'http://localhost:11434/v1/chat/completions'; + const API_KEY = ''; // 本地模型不需要API Key + const MODEL = 'qwen2.5:7b'; // 根据实际使用的本地模型修改 // 构建系统提示词 const SYSTEM_PROMPT = [ @@ -426,20 +427,26 @@ export default { // 提取纯文本内容(去除HTML标签) const textContent = reportContent.replace(/<[^>]*>/g, '').substring(0, 3000); - const prompt = `${SYSTEM_PROMPT}\n\n重要:请直接输出结果,不要包含任何思考过程、标签或标签。\n\n报告标题:${reportTitle}\n报告类型:${reportType}\n报告内容:${textContent}`; + const userPrompt = `重要:请直接输出结果,不要包含任何思考过程、标签或标签。\n\n报告标题:${reportTitle}\n报告类型:${reportType}\n报告内容:${textContent}`; try { - const { data } = await axios.post(OLLAMA_URL, { + const { data } = await axios.post(API_URL, { model: MODEL, - prompt: prompt, + messages: [ + { role: 'system', content: SYSTEM_PROMPT }, + { role: 'user', content: userPrompt } + ], temperature: 0.2, - num_predict: 1000, - stream: false + max_tokens: 1000 }, { + headers: { + 'Authorization': `Bearer ${API_KEY}`, + 'Content-Type': 'application/json' + }, timeout: 60000 // 60秒超时 }); - let rawResponse = data?.response ?? '无法解析模型输出'; + let rawResponse = data?.choices?.[0]?.message?.content ?? '无法解析模型输出'; // 过滤掉思考过程标签 rawResponse = rawResponse diff --git a/xinli-ui/src/views/psychology/report/index.vue b/xinli-ui/src/views/psychology/report/index.vue index 404197a8..3484d408 100644 --- a/xinli-ui/src/views/psychology/report/index.vue +++ b/xinli-ui/src/views/psychology/report/index.vue @@ -709,9 +709,10 @@ export default { }, /** AI分析报告内容 */ async generateAIAnalysis(reportContent, reportTitle, reportType) { - // Ollama API配置 - const OLLAMA_URL = 'http://192.168.0.106:11434/api/generate'; - const MODEL = 'deepseek-r1:32b'; + // 本地大模型API配置(如Ollama) + const API_URL = 'http://localhost:11434/v1/chat/completions'; + const API_KEY = ''; // 本地模型不需要API Key + const MODEL = 'qwen2.5:7b'; // 根据实际使用的本地模型修改 // 构建系统提示词 const SYSTEM_PROMPT = [ @@ -727,20 +728,26 @@ export default { // 提取纯文本内容(去除HTML标签) const textContent = reportContent.replace(/<[^>]*>/g, '').substring(0, 3000); - const prompt = `${SYSTEM_PROMPT}\n\n重要:请直接输出结果,不要包含任何思考过程、标签或标签。\n\n报告标题:${reportTitle}\n报告类型:${reportType}\n报告内容:${textContent}`; + const userPrompt = `重要:请直接输出结果,不要包含任何思考过程、标签或标签。\n\n报告标题:${reportTitle}\n报告类型:${reportType}\n报告内容:${textContent}`; try { - const { data } = await axios.post(OLLAMA_URL, { + const { data } = await axios.post(API_URL, { model: MODEL, - prompt: prompt, + messages: [ + { role: 'system', content: SYSTEM_PROMPT }, + { role: 'user', content: userPrompt } + ], temperature: 0.2, - num_predict: 1000, - stream: false + max_tokens: 1000 }, { + headers: { + 'Authorization': `Bearer ${API_KEY}`, + 'Content-Type': 'application/json' + }, timeout: 60000 // 60秒超时 }); - let rawResponse = data?.response ?? ''; + let rawResponse = data?.choices?.[0]?.message?.content ?? ''; // 过滤掉思考过程标签 rawResponse = rawResponse diff --git a/xinli-ui/src/views/psychology/scale/index.vue b/xinli-ui/src/views/psychology/scale/index.vue index a7f74303..ac363064 100644 --- a/xinli-ui/src/views/psychology/scale/index.vue +++ b/xinli-ui/src/views/psychology/scale/index.vue @@ -35,7 +35,7 @@ /> - + { - if (!item || !item.scaleId) { + // 只处理状态为'3'(暂停)的记录 + if (!item || !item.scaleId || item.status !== '3') { return } const existing = map[item.scaleId] @@ -205,7 +206,8 @@ export default { cancelButtonText: '取消', type: 'warning' }).then(() => { - this.createNewAssessment(test) + // 先删除旧的暂停记录,再创建新测评 + this.deleteAndCreateNew(pausedRecord, test) }) }) return @@ -294,6 +296,28 @@ export default { }) }, + // 删除旧记录并创建新测评 + deleteAndCreateNew(pausedRecord, test) { + if (!pausedRecord || !pausedRecord.assessmentId) { + this.createAssessment(test) + return + } + + this.loading = true + const { delAssessment } = require("@/api/psychology/assessment") + delAssessment(pausedRecord.assessmentId) + .then(() => { + // 删除成功后,刷新暂停列表并创建新测评 + this.loadPausedList() + this.createAssessment(test) + }) + .catch(error => { + console.error("删除暂停记录失败:", error) + this.loading = false + Message.error("删除旧记录失败,请稍后重试") + }) + }, + // 获取量表类型名称 getScaleTypeName(type) { // 这里可以根据实际需求返回类型名称