From 66773d3577da29877e9f6328c685a6f54324bb97 Mon Sep 17 00:00:00 2001 From: xiao12feng Date: Fri, 28 Nov 2025 17:31:13 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../psychology/PsyUserProfileServiceImpl.java | 18 +- .../system/psychology/PsyScaleMapper.xml | 4 +- .../psychology/PsyScalePermissionMapper.xml | 1 + .../psychology/PsyUserProfileMapper.xml | 1 - xinli-ui/src/router/index.js | 38 +-- xinli-ui/src/utils/request.js | 9 + .../src/views/psychology/assessment/index.vue | 3 +- .../src/views/psychology/assessment/start.vue | 5 +- .../views/psychology/assessment/taking.vue | 122 +++++-- .../src/views/psychology/permission/index.vue | 24 +- .../src/views/psychology/profile/index.vue | 299 ++++++++++-------- .../views/psychology/questionnaire/index.vue | 28 ++ .../views/psychology/report/comprehensive.vue | 71 ++++- xinli-ui/src/views/psychology/scale/index.vue | 254 ++++++++++++++- xinli-ui/src/views/student/tests.vue | 23 +- 15 files changed, 679 insertions(+), 221 deletions(-) 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 84916dd7..ae540b85 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 @@ -549,13 +549,19 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService if (failureNum > 0) { + StringBuilder resultMsg = new StringBuilder(); + // 如果有成功的数据,先显示成功信息 + if (successNum > 0) + { + resultMsg.append(successMsg).append("

"); + } + // 再显示失败信息 failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:"); - if (successNum > 0) - { - failureMsg.append("

").append(successMsg); - } - importProgressManager.finishFailure(progressKey, failureMsg.toString()); - throw new ServiceException(failureMsg.toString()); + resultMsg.append(failureMsg); + + String finalMsg = resultMsg.toString(); + importProgressManager.finishFailure(progressKey, finalMsg); + throw new ServiceException(finalMsg); } if (successNum > 0) diff --git a/ry-xinli-system/src/main/resources/mapper/system/psychology/PsyScaleMapper.xml b/ry-xinli-system/src/main/resources/mapper/system/psychology/PsyScaleMapper.xml index a74059dc..b606033f 100644 --- a/ry-xinli-system/src/main/resources/mapper/system/psychology/PsyScaleMapper.xml +++ b/ry-xinli-system/src/main/resources/mapper/system/psychology/PsyScaleMapper.xml @@ -26,6 +26,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -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 diff --git a/ry-xinli-system/src/main/resources/mapper/system/psychology/PsyScalePermissionMapper.xml b/ry-xinli-system/src/main/resources/mapper/system/psychology/PsyScalePermissionMapper.xml index e81148a7..631e4435 100644 --- a/ry-xinli-system/src/main/resources/mapper/system/psychology/PsyScalePermissionMapper.xml +++ b/ry-xinli-system/src/main/resources/mapper/system/psychology/PsyScalePermissionMapper.xml @@ -20,6 +20,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + 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 6242bb0a..224e8fac 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 @@ -182,7 +182,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" sentence_end_date = #{sentenceEndDate}, entry_date = #{entryDate}, info_number = #{infoNumber}, - status = #{status}, update_by = #{updateBy}, remark = #{remark}, update_time = sysdate() diff --git a/xinli-ui/src/router/index.js b/xinli-ui/src/router/index.js index c30c853c..169cc1ee 100644 --- a/xinli-ui/src/router/index.js +++ b/xinli-ui/src/router/index.js @@ -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'] } } ] diff --git a/xinli-ui/src/utils/request.js b/xinli-ui/src/utils/request.js index 11908135..032c9441 100644 --- a/xinli-ui/src/utils/request.js +++ b/xinli-ui/src/utils/request.js @@ -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') && diff --git a/xinli-ui/src/views/psychology/assessment/index.vue b/xinli-ui/src/views/psychology/assessment/index.vue index 0993faea..1a77fe0c 100644 --- a/xinli-ui/src/views/psychology/assessment/index.vue +++ b/xinli-ui/src/views/psychology/assessment/index.vue @@ -41,7 +41,7 @@ icon="el-icon-plus" size="mini" @click="handleStartAssessment" - v-hasPermi="['psychology:assessment:add']" + v-hasPermi="['psychology:assessment:start']" >开始测评 @@ -164,7 +164,6 @@ 答题详情 - diff --git a/xinli-ui/src/views/psychology/assessment/start.vue b/xinli-ui/src/views/psychology/assessment/start.vue index a060157b..65e1df67 100644 --- a/xinli-ui/src/views/psychology/assessment/start.vue +++ b/xinli-ui/src/views/psychology/assessment/start.vue @@ -145,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) { diff --git a/xinli-ui/src/views/psychology/assessment/taking.vue b/xinli-ui/src/views/psychology/assessment/taking.vue index 2dd33f30..f2e3fc90 100644 --- a/xinli-ui/src/views/psychology/assessment/taking.vue +++ b/xinli-ui/src/views/psychology/assessment/taking.vue @@ -38,10 +38,11 @@ size="small" @click="speakText(currentItem.itemContent)" :disabled="!isTtsSupported" - class="tts-btn" + :class="['tts-btn', isSpeaking ? 'speaking' : '']" title="朗读题干" > - 朗读题干 + @@ -55,10 +56,11 @@ size="mini" @click="speakText(option.optionContent)" :disabled="!isTtsSupported" - class="option-tts-btn" + :class="['option-tts-btn', isSpeaking ? 'speaking' : '']" title="朗读选项" > - 朗读选项 + @@ -74,10 +76,11 @@ size="mini" @click="speakText(option.optionContent)" :disabled="!isTtsSupported" - class="option-tts-btn" + :class="['option-tts-btn', isSpeaking ? 'speaking' : '']" title="朗读选项" > - 朗读选项 + @@ -134,7 +137,8 @@ export default { isTtsSupported: false, synth: null, currentUtterance: null, - voiceIcon + voiceIcon, + isSpeaking: false }; }, computed: { @@ -208,49 +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) => { + this.isSpeaking = false; const errorType = event.error || ''; - // 忽略 interrupted 和 canceled 错误(这些是正常的中断,不是真正的错误) + // 忽略正常的中断 const ignoredErrors = ['interrupted', 'canceled']; if (ignoredErrors.includes(errorType)) { - console.log('TTS 被中断(正常情况):', errorType); return; } console.error('TTS 错误:', event); - if (hasStarted) { - return; - } - 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('文本过长,无法朗读'); - } + // 不再显示错误提示,改为静默失败 }; + try { + // 为了兼容移动端,需要在用户交互后立即调用 this.synth.speak(this.currentUtterance); } catch (error) { + this.isSpeaking = false; console.error('调用 speak 失败:', error); - this.$message.error('语音朗读失败:无法启动语音合成'); } }, /** 停止朗读 */ @@ -259,6 +276,7 @@ export default { this.synth.cancel(); } this.currentUtterance = null; + this.isSpeaking = false; }, /** 错误信息 */ getErrorMessage(errorType) { @@ -446,10 +464,26 @@ export default { /** 退出 */ handleExit() { this.$modal.confirm('确定要退出测评吗?已答题目将会保存。').then(() => { - // 根据用户角色跳转到相应页面 - const roles = this.$store.getters.roles || []; - const isStudent = roles.some(role => role === 'student' || role.includes('学员')); - this.$router.push(isStudent ? '/student/tests' : '/psychology/assessment'); + this.loading = true; + // 先等待一小段时间,确保最后的答案保存请求发出 + setTimeout(() => { + // 退出前先暂停测评,保存进度 + pauseAssessment(this.assessmentId).then(() => { + this.loading = false; + this.$modal.msgSuccess("测评进度已保存"); + // 根据用户角色跳转到相应页面 + const roles = this.$store.getters.roles || []; + const isStudent = roles.some(role => role === 'student' || role.includes('学员')); + this.$router.push(isStudent ? '/student/tests' : '/psychology/assessment'); + }).catch(error => { + console.error('保存测评进度失败:', error); + this.loading = false; + // 即使保存失败也允许退出,因为答案已经自动保存了 + const roles = this.$store.getters.roles || []; + const isStudent = roles.some(role => role === 'student' || role.includes('学员')); + this.$router.push(isStudent ? '/student/tests' : '/psychology/assessment'); + }); + }, 500); // 等待500ms,确保答案保存请求已发送 }); }, /** 提交测评 */ @@ -794,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); + } +} + diff --git a/xinli-ui/src/views/psychology/permission/index.vue b/xinli-ui/src/views/psychology/permission/index.vue index b1fbe163..4fac9983 100644 --- a/xinli-ui/src/views/psychology/permission/index.vue +++ b/xinli-ui/src/views/psychology/permission/index.vue @@ -95,9 +95,10 @@ - - - - + + + +