权限、用户bug功能完善

This commit is contained in:
xiao12feng 2025-12-01 11:58:40 +08:00
parent 8fb641893c
commit 151cae375e
14 changed files with 658 additions and 161 deletions

View File

@ -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<SysUser> userList);
/**
* 批量查询用户名是否存在
*
* @param userNames 用户名列表
* @return 已存在的用户列表
*/
public List<SysUser> selectUsersByUserNames(@Param("userNames") List<String> userNames);
}

View File

@ -81,5 +81,29 @@ public interface PsyUserProfileMapper
* @return 结果
*/
public int deleteProfileByIds(Long[] profileIds);
/**
* 批量查询档案根据信息编号列表
*
* @param infoNumbers 信息编号列表
* @return 档案集合
*/
public List<PsyUserProfile> selectProfilesByInfoNumbers(List<String> infoNumbers);
/**
* 批量插入档案
*
* @param profileList 档案列表
* @return 结果
*/
public int batchInsertProfiles(List<PsyUserProfile> profileList);
/**
* 批量更新档案
*
* @param profileList 档案列表
* @return 结果
*/
public int batchUpdateProfiles(List<PsyUserProfile> profileList);
}

View File

@ -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<SysUser> validUsers = new ArrayList<>();
List<String> 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("<br/>" + 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("<br/>" + successNum + "、账号 " + user.getUserName() + " 更新成功");
}
else
{
failureNum++;
failureMsg.append("<br/>" + 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 = "<br/>" + failureNum + "、账号 " + user.getUserName() + " 导入失败:";
String msg = "<br/>" + failureNum + "、账号 " + user.getUserName() + " 验证失败:";
failureMsg.append(msg + e.getMessage());
log.error(msg, e);
}
}
if (validUsers.isEmpty())
{
throw new ServiceException("没有有效的用户数据可以导入!");
}
// 第二步批量查询已存在的用户只查一次数据库
List<SysUser> existingUsers = userMapper.selectUsersByUserNames(userNameList);
Map<String, SysUser> existingUserMap = existingUsers.stream()
.collect(Collectors.toMap(SysUser::getUserName, u -> u));
// 第三步分类处理
List<SysUser> usersToInsert = new ArrayList<>();
List<SysUser> 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("<br/>").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<SysUser> batch = usersToInsert.subList(i, end);
try
{
int inserted = userMapper.batchInsertUser(batch);
successNum += inserted;
for (SysUser user : batch)
{
successMsg.append("<br/>").append(successNum).append("、账号 ").append(user.getUserName()).append(" 导入成功");
}
}
catch (Exception e)
{
// 如果批量插入失败降级为逐条插入
log.warn("批量插入失败,降级为逐条插入", e);
for (SysUser user : batch)
{
try
{
userMapper.insertUser(user);
successNum++;
successMsg.append("<br/>").append(successNum).append("、账号 ").append(user.getUserName()).append(" 导入成功");
}
catch (Exception ex)
{
failureNum++;
failureMsg.append("<br/>").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("<br/>").append(successNum).append("、账号 ").append(user.getUserName()).append(" 更新成功");
}
catch (Exception e)
{
failureNum++;
failureMsg.append("<br/>").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();
}

View File

@ -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<PsyUserProfile> validProfiles = new ArrayList<>();
List<String> infoNumbers = new ArrayList<>();
for (PsyUserProfile profile : profileList)
{
try
{
// 设置创建者
profile.setCreateBy(operName);
// 验证信息编号
if (StringUtils.isEmpty(profile.getInfoNumber()))
{
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、档案信息编号为空");
importProgressManager.recordFailure(progressKey);
continue;
}
// 根据信息编号查询是否存在
PsyUserProfile existProfile = profileMapper.selectProfileByInfoNumber(profile.getInfoNumber());
if (StringUtils.isNull(existProfile))
{
// 新增档案
this.insertProfile(profile);
successNum++;
successMsg.append("<br/>").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 导入成功");
importProgressManager.recordSuccess(progressKey);
}
else if (isUpdateSupport)
{
// 更新档案
profile.setProfileId(existProfile.getProfileId());
profile.setUserId(existProfile.getUserId());
profile.setUpdateBy(operName);
this.updateProfile(profile);
successNum++;
successMsg.append("<br/>").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 更新成功");
importProgressManager.recordSuccess(progressKey);
}
else
{
failureNum++;
failureMsg.append("<br/>").append(failureNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 已存在");
importProgressManager.recordFailure(progressKey);
}
}
catch (Exception e)
// 设置创建者
profile.setCreateBy(operName);
// 验证信息编号
if (StringUtils.isEmpty(profile.getInfoNumber()))
{
failureNum++;
String msg = "<br/>" + failureNum + "、信息编号 " + profile.getInfoNumber() + " 导入失败:";
failureMsg.append(msg).append(e.getMessage());
failureMsg.append("<br/>").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<PsyUserProfile> existingProfiles = profileMapper.selectProfilesByInfoNumbers(infoNumbers);
java.util.Map<String, PsyUserProfile> existingMap = new java.util.HashMap<>();
for (PsyUserProfile existing : existingProfiles)
{
existingMap.put(existing.getInfoNumber(), existing);
}
// 3. 分类处理新增列表和更新列表
List<PsyUserProfile> toInsertList = new ArrayList<>();
List<PsyUserProfile> 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("<br/>").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<PsyUserProfile> batch = toInsertList.subList(i, end);
try
{
int insertCount = profileMapper.batchInsertProfiles(batch);
for (PsyUserProfile profile : batch)
{
successNum++;
successMsg.append("<br/>").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("<br/>").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 导入成功");
importProgressManager.recordSuccess(progressKey);
}
catch (Exception ex)
{
failureNum++;
String msg = "<br/>" + 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<PsyUserProfile> batch = toUpdateList.subList(i, end);
try
{
int updateCount = profileMapper.batchUpdateProfiles(batch);
for (PsyUserProfile profile : batch)
{
successNum++;
successMsg.append("<br/>").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("<br/>").append(successNum).append("、信息编号 ").append(profile.getInfoNumber()).append(" 更新成功");
importProgressManager.recordSuccess(progressKey);
}
catch (Exception ex)
{
failureNum++;
String msg = "<br/>" + failureNum + "、信息编号 " + profile.getInfoNumber() + " 更新失败:";
failureMsg.append(msg).append(ex.getMessage());
importProgressManager.recordFailure(progressKey);
log.error(msg, ex);
}
}
}
}
}

View File

@ -232,5 +232,30 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{userId}
</foreach>
</delete>
<!-- 批量插入用户 -->
<insert id="batchInsertUser" parameterType="java.util.List">
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
<foreach collection="list" item="item" separator=",">
(
#{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}
)
</foreach>
</insert>
<!-- 批量查询用户名 -->
<select id="selectUsersByUserNames" resultMap="SysUserResult">
<include refid="selectUserVo"/>
where u.user_name in
<foreach collection="userNames" item="userName" open="(" separator="," close=")">
#{userName}
</foreach>
and u.del_flag = '0'
</select>
</mapper>

View File

@ -275,5 +275,64 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
order by u.create_time desc
</select>
<!-- 批量查询档案(根据信息编号列表) -->
<select id="selectProfilesByInfoNumbers" resultMap="PsyUserProfileResult">
<include refid="selectProfileVo"/>
from psy_user_profile p
left join sys_user u on p.user_id = u.user_id
where p.info_number in
<foreach item="infoNumber" collection="list" open="(" separator="," close=")">
#{infoNumber}
</foreach>
</select>
<!-- 批量插入档案 -->
<insert id="batchInsertProfiles" parameterType="java.util.List">
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
<foreach item="item" collection="list" separator=",">
(
#{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}
)
</foreach>
</insert>
<!-- 批量更新档案 -->
<update id="batchUpdateProfiles" parameterType="java.util.List">
<foreach item="item" collection="list" separator=";">
update psy_user_profile
<set>
<if test="item.profileType != null and item.profileType != ''">profile_type = #{item.profileType},</if>
<if test="item.profileData != null">profile_data = #{item.profileData},</if>
<if test="item.avatar != null and item.avatar != ''">avatar = #{item.avatar},</if>
<if test="item.idCard != null and item.idCard != ''">id_card = #{item.idCard},</if>
<if test="item.birthday != null">birthday = #{item.birthday},</if>
<if test="item.prison != null and item.prison != ''">prison = #{item.prison},</if>
<if test="item.prisonArea != null and item.prisonArea != ''">prison_area = #{item.prisonArea},</if>
<if test="item.gender != null and item.gender != ''">gender = #{item.gender},</if>
<if test="item.nation != null and item.nation != ''">nation = #{item.nation},</if>
<if test="item.educationLevel != null and item.educationLevel != ''">education_level = #{item.educationLevel},</if>
<if test="item.crimeName != null and item.crimeName != ''">crime_name = #{item.crimeName},</if>
<if test="item.sentenceTerm != null and item.sentenceTerm != ''">sentence_term = #{item.sentenceTerm},</if>
<if test="item.sentenceStartDate != null">sentence_start_date = #{item.sentenceStartDate},</if>
<if test="item.sentenceEndDate != null">sentence_end_date = #{item.sentenceEndDate},</if>
<if test="item.entryDate != null">entry_date = #{item.entryDate},</if>
<if test="item.updateBy != null and item.updateBy != ''">update_by = #{item.updateBy},</if>
update_time = sysdate(),
<if test="item.remark != null">remark = #{item.remark},</if>
</set>
where profile_id = #{item.profileId}
</foreach>
</update>
</mapper>

View File

@ -33,17 +33,31 @@
<div class="question-number"> {{ currentIndex + 1 }} </div>
<div class="question-content-wrapper">
<div class="question-content">{{ currentItem.itemContent }}</div>
<el-button
type="text"
size="small"
@click="speakText(currentItem.itemContent)"
:disabled="!isTtsSupported"
:class="['tts-btn', isSpeaking ? 'speaking' : '']"
title="朗读题干"
>
<i :class="isSpeaking ? 'el-icon-video-pause' : 'el-icon-service'"
style="font-size: 18px; color: #409EFF;"></i>
</el-button>
<div class="tts-buttons">
<el-button
type="text"
size="small"
@click="speakCurrentQuestion"
:disabled="!isTtsSupported"
:class="['tts-btn-all', isSpeaking ? 'speaking' : '']"
title="朗读题目和所有选项"
>
<i :class="isSpeaking ? 'el-icon-video-pause' : 'el-icon-s-promotion'"
style="font-size: 18px; color: #67C23A;"></i>
<span style="font-size: 12px; margin-left: 4px;">朗读全部</span>
</el-button>
<el-button
type="text"
size="small"
@click="speakText(currentItem.itemContent)"
:disabled="!isTtsSupported"
:class="['tts-btn', isSpeaking ? 'speaking' : '']"
title="只朗读题干"
>
<i :class="isSpeaking ? 'el-icon-video-pause' : 'el-icon-service'"
style="font-size: 18px; color: #409EFF;"></i>
</el-button>
</div>
</div>
<!-- 单选题 -->
@ -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 {

View File

@ -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

View File

@ -21,16 +21,31 @@
<div class="question-number"> {{ currentIndex + 1 }} </div>
<div class="question-content-wrapper">
<div class="question-content">{{ currentItem.itemContent }}</div>
<el-button
type="text"
size="small"
@click="speakText(currentItem.itemContent)"
:disabled="!isTtsSupported"
class="tts-btn"
title="朗读题目"
>
<img :src="voiceIcon" alt="朗读题目" class="tts-icon" />
</el-button>
<div class="tts-buttons">
<el-button
type="text"
size="small"
@click="speakCurrentQuestion"
:disabled="!isTtsSupported"
:class="['tts-btn-all', isSpeaking ? 'speaking' : '']"
title="朗读题目和所有选项"
>
<i :class="isSpeaking ? 'el-icon-video-pause' : 'el-icon-s-promotion'"
style="font-size: 18px; color: #67C23A;"></i>
<span style="font-size: 12px; margin-left: 4px;">朗读全部</span>
</el-button>
<el-button
type="text"
size="small"
@click="speakText(currentItem.itemContent)"
:disabled="!isTtsSupported"
:class="['tts-btn', isSpeaking ? 'speaking' : '']"
title="只朗读题干"
>
<i :class="isSpeaking ? 'el-icon-video-pause' : 'el-icon-service'"
style="font-size: 18px; color: #409EFF;"></i>
</el-button>
</div>
</div>
<div class="question-info" v-if="currentItem.score">
<el-tag type="info">分值{{ currentItem.score }}</el-tag>
@ -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 {

View File

@ -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

View File

@ -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';
// APIOllama
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重要:请直接输出结果,不要包含任何思考过程、<think>标签或<think>标签。\n\n报告标题${reportTitle}\n报告类型${reportType}\n报告内容${textContent}`;
const userPrompt = `重要:请直接输出结果,不要包含任何思考过程、<think>标签或<think>标签。\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

View File

@ -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';
// APIOllama
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重要:请直接输出结果,不要包含任何思考过程、<think>标签或</think>标签。\n\n报告标题${reportTitle}\n报告类型${reportType}\n报告内容${textContent}`;
const userPrompt = `重要:请直接输出结果,不要包含任何思考过程、<think>标签或</think>标签。\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

View File

@ -35,7 +35,7 @@
/>
</el-select>
</el-form-item>
<el-form-item label="量表类型" prop="scaleType">
<el-form-item label="量表类型" prop="scaleType" v-show="false">
<el-select v-model="queryParams.scaleType" placeholder="量表类型" clearable>
<el-option
v-for="dict in dict.type.psy_scale_type"

View File

@ -155,7 +155,8 @@ export default {
const rows = response.data || []
const map = {}
rows.forEach(item => {
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) {
//