修改问题

This commit is contained in:
xiao12feng 2025-11-28 17:31:13 +08:00
parent 12d943db6c
commit 66773d3577
15 changed files with 679 additions and 221 deletions

View File

@ -549,13 +549,19 @@ public class PsyUserProfileServiceImpl implements IPsyUserProfileService
if (failureNum > 0)
{
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
StringBuilder resultMsg = new StringBuilder();
// 如果有成功的数据先显示成功信息
if (successNum > 0)
{
failureMsg.append("<br/><br/>").append(successMsg);
resultMsg.append(successMsg).append("<br/><br/>");
}
importProgressManager.finishFailure(progressKey, failureMsg.toString());
throw new ServiceException(failureMsg.toString());
// 再显示失败信息
failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
resultMsg.append(failureMsg);
String finalMsg = resultMsg.toString();
importProgressManager.finishFailure(progressKey, finalMsg);
throw new ServiceException(finalMsg);
}
if (successNum > 0)

View File

@ -26,6 +26,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
<result property="sourceType" column="source_type" />
</resultMap>
<sql id="selectScaleVo">
@ -34,7 +35,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
COALESCE(COUNT(i.item_id), 0) as item_count,
s.estimated_time, s.target_population,
s.author, s.source, s.reference, s.status, s.sort_order, s.create_by, s.create_time,
s.update_by, s.update_time, s.remark
s.update_by, s.update_time, s.remark,
'scale' as source_type
from psy_scale s
left join psy_scale_item i on s.scale_id = i.scale_id
</sql>

View File

@ -20,6 +20,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
<result property="scaleName" column="scale_name" />
<result property="sourceType" column="source_type" />
<result property="deptName" column="dept_name" />
<result property="roleName" column="role_name" />
<result property="userName" column="user_name" />

View File

@ -182,7 +182,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="sentenceEndDate != null">sentence_end_date = #{sentenceEndDate}, </if>
<if test="entryDate != null">entry_date = #{entryDate}, </if>
<if test="infoNumber != null">info_number = #{infoNumber}, </if>
<if test="status != null and status != ''">status = #{status}, </if>
<if test="updateBy != null">update_by = #{updateBy}, </if>
<if test="remark != null">remark = #{remark}, </if>
update_time = sysdate()

View File

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

View File

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

View File

@ -41,7 +41,7 @@
icon="el-icon-plus"
size="mini"
@click="handleStartAssessment"
v-hasPermi="['psychology:assessment:add']"
v-hasPermi="['psychology:assessment:start']"
>开始测评</el-button>
</el-col>
<el-col :span="1.5">
@ -164,7 +164,6 @@
<el-divider content-position="left">答题详情</el-divider>
<el-table :data="answerDetailList" border style="width: 100%">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column label="题目序号" prop="itemNumber" width="100" align="center" />
<el-table-column label="题目内容" prop="itemContent" min-width="200" :show-overflow-tooltip="true" />
<el-table-column label="题目类型" prop="itemType" width="100" align="center">

View File

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

View File

@ -38,10 +38,11 @@
size="small"
@click="speakText(currentItem.itemContent)"
:disabled="!isTtsSupported"
class="tts-btn"
:class="['tts-btn', isSpeaking ? 'speaking' : '']"
title="朗读题干"
>
<img :src="voiceIcon" alt="朗读题干" class="tts-icon" />
<i :class="isSpeaking ? 'el-icon-video-pause' : 'el-icon-service'"
style="font-size: 18px; color: #409EFF;"></i>
</el-button>
</div>
@ -55,10 +56,11 @@
size="mini"
@click="speakText(option.optionContent)"
:disabled="!isTtsSupported"
class="option-tts-btn"
:class="['option-tts-btn', isSpeaking ? 'speaking' : '']"
title="朗读选项"
>
<img :src="voiceIcon" alt="朗读选项" class="tts-icon" />
<i :class="isSpeaking ? 'el-icon-video-pause' : 'el-icon-service'"
style="font-size: 16px; color: #409EFF;"></i>
</el-button>
</div>
</el-radio-group>
@ -74,10 +76,11 @@
size="mini"
@click="speakText(option.optionContent)"
:disabled="!isTtsSupported"
class="option-tts-btn"
:class="['option-tts-btn', isSpeaking ? 'speaking' : '']"
title="朗读选项"
>
<img :src="voiceIcon" alt="朗读选项" class="tts-icon" />
<i :class="isSpeaking ? 'el-icon-video-pause' : 'el-icon-service'"
style="font-size: 16px; color: #409EFF;"></i>
</el-button>
</div>
</el-checkbox-group>
@ -134,7 +137,8 @@ export default {
isTtsSupported: false,
synth: null,
currentUtterance: null,
voiceIcon
voiceIcon,
isSpeaking: false
};
},
computed: {
@ -208,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(() => {
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);
}
}
</style>

View File

@ -95,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>
@ -252,6 +253,8 @@
<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
@ -745,6 +748,21 @@ export default {
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) {

View File

@ -1,10 +1,10 @@
<template>
<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="userName">
<el-form-item label="罪犯姓名" prop="userName">
<el-input
v-model="queryParams.userName"
placeholder="请输入用户名称"
placeholder="请输入罪犯姓名"
clearable
@keyup.enter.native="handleQuery"
/>
@ -488,6 +488,10 @@ export default {
profileList: [],
// ID
studentRoleId: null,
// ID
studentUserIds: null,
//
studentUserTotal: 0,
//
title: "",
//
@ -600,10 +604,8 @@ export default {
this.stopImportProgressPolling()
},
created() {
this.getStudentRoleId().then(() => {
this.getList()
this.loadFilterOptions()
})
this.getDeptTree()
this.getConfigKey("sys.user.initPassword").then(response => {
this.initPassword = response.msg
@ -643,17 +645,83 @@ export default {
})
})
},
/** 加载学员用户ID列表缓存 */
loadStudentUserIds() {
return new Promise((resolve, reject) => {
if (this.studentUserIds !== null) {
//
resolve(this.studentUserIds)
return
}
if (!this.studentRoleId) {
resolve(new Set())
return
}
// ID
const studentUserIds = new Set()
let pageNum = 1
const pageSize = 500 // 500
let total = 0
const fetchPage = () => {
return allocatedUserList({
roleId: this.studentRoleId,
status: '0',
pageNum: pageNum,
pageSize: pageSize
}).then(response => {
const rows = response.rows || []
const responseTotal = response.total || 0
if (total === 0 && responseTotal > 0) {
total = responseTotal
}
rows.forEach(user => {
if (user.userId) {
studentUserIds.add(user.userId)
}
})
//
if (rows.length === pageSize && (total === 0 || studentUserIds.size < total)) {
pageNum++
return fetchPage()
} else {
//
this.studentUserIds = studentUserIds
this.studentUserTotal = studentUserIds.size
resolve(studentUserIds)
}
}).catch(error => {
console.error("获取学员用户列表失败:", error)
// 使Set
this.studentUserIds = new Set()
this.studentUserTotal = 0
reject(error)
})
}
fetchPage()
})
},
/** 查询档案列表 */
getList() {
this.loading = true
//
// 使
const queryParams = {
...this.queryParams,
pageNum: 1,
pageSize: 10000 //
pageNum: this.queryParams.pageNum || 1,
pageSize: this.queryParams.pageSize || 10
}
//
listProfile(queryParams).then(response => {
const rows = response.rows || []
const total = response.total || 0
//
rows.forEach(row => {
if (row.status === null || row.status === undefined || row.status === '') {
@ -662,16 +730,12 @@ export default {
row.status = String(row.status) //
}
})
// ID
if (this.studentRoleId) {
this.filterStudentUsers(rows).then(filteredRows => {
//
const pageNum = this.queryParams.pageNum || 1
const pageSize = this.queryParams.pageSize || 10
const start = (pageNum - 1) * pageSize
const end = start + pageSize
this.profileList = filteredRows.slice(start, end)
this.total = filteredRows.length
// 使
//
this.profileList = rows
this.total = total
this.selectedRows = []
this.ids = []
this.single = true
@ -682,85 +746,37 @@ export default {
}
})
this.loading = false
}).catch(error => {
console.error("过滤学员用户失败:", error)
//
const pageNum = this.queryParams.pageNum || 1
const pageSize = this.queryParams.pageSize || 10
const start = (pageNum - 1) * pageSize
const end = start + pageSize
this.profileList = rows.slice(start, end)
this.total = rows.length
this.selectedRows = []
this.ids = []
this.single = true
this.multiple = true
this.$nextTick(() => {
if (this.$refs.profileTable) {
this.$refs.profileTable.clearSelection()
}
})
this.loading = false
})
} else {
// ID
this.getStudentRoleId().then(() => {
this.filterStudentUsers(rows).then(filteredRows => {
//
const pageNum = this.queryParams.pageNum || 1
const pageSize = this.queryParams.pageSize || 10
const start = (pageNum - 1) * pageSize
const end = start + pageSize
this.profileList = filteredRows.slice(start, end)
this.total = filteredRows.length
this.selectedRows = []
this.ids = []
this.single = true
this.multiple = true
this.$nextTick(() => {
if (this.$refs.profileTable) {
this.$refs.profileTable.clearSelection()
}
})
this.loading = false
}).catch(error => {
console.error("过滤学员用户失败:", error)
const pageNum = this.queryParams.pageNum || 1
const pageSize = this.queryParams.pageSize || 10
const start = (pageNum - 1) * pageSize
const end = start + pageSize
this.profileList = rows.slice(start, end)
this.total = rows.length
this.selectedRows = []
this.ids = []
this.single = true
this.multiple = true
this.$nextTick(() => {
if (this.$refs.profileTable) {
this.$refs.profileTable.clearSelection()
}
})
this.loading = false
})
})
}
}).catch(error => {
console.error("查询档案列表失败:", error)
this.loading = false
})
},
/** 加载监狱/监区筛选项 */
/** 加载监狱/监区筛选项(优化:分批加载) */
loadFilterOptions() {
const params = {
pageNum: 1,
pageSize: 10000
}
listProfile(params).then(response => {
const rows = response.rows || []
const handleRows = filteredRows => {
// /
const prisonSet = new Set()
const prisonAreaSet = new Set()
filteredRows.forEach(row => {
let pageNum = 1
const pageSize = 500 // 500
let total = 0
let loadedCount = 0
const loadPage = () => {
return listProfile({
pageNum: pageNum,
pageSize: pageSize
}).then(response => {
const rows = response.rows || []
const responseTotal = response.total || 0
if (total === 0 && responseTotal > 0) {
total = responseTotal
}
loadedCount += rows.length
//
rows.forEach(row => {
if (row && row.prison) {
prisonSet.add(row.prison)
}
@ -768,51 +784,60 @@ export default {
prisonAreaSet.add(row.prisonArea)
}
})
this.filterPrisonOptions = Array.from(prisonSet)
this.filterPrisonAreaOptions = Array.from(prisonAreaSet)
}
if (this.studentRoleId) {
this.filterStudentUsers(rows).then(filteredRows => {
handleRows(filteredRows)
}).catch(() => {
handleRows(rows)
})
//
if (rows.length === pageSize && (total === 0 || loadedCount < total)) {
pageNum++
return loadPage()
} else {
handleRows(rows)
//
this.filterPrisonOptions = Array.from(prisonSet).sort()
this.filterPrisonAreaOptions = Array.from(prisonAreaSet).sort()
}
}).catch(error => {
console.error("加载监狱筛选项失败:", error)
// 使
this.filterPrisonOptions = Array.from(prisonSet).sort()
this.filterPrisonAreaOptions = Array.from(prisonAreaSet).sort()
})
}
// ID
//
loadPage()
},
/** 过滤学员用户 */
/** 过滤学员用户使用缓存的学员用户ID列表 */
filterStudentUsers(rows) {
return new Promise((resolve, reject) => {
if (!this.studentRoleId || rows.length === 0) {
if (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)
}
})
//
// ID使
if (this.studentUserIds && this.studentUserIds.size > 0) {
const filteredRows = rows.filter(row => {
return row.userId && studentUserIds.has(row.userId)
return row.userId && this.studentUserIds.has(row.userId)
})
resolve(filteredRows)
return
}
//
if (!this.studentRoleId) {
resolve(rows)
return
}
this.loadStudentUserIds().then(() => {
const filteredRows = rows.filter(row => {
return row.userId && this.studentUserIds.has(row.userId)
})
resolve(filteredRows)
}).catch(error => {
console.error("获取学员用户列表失败:", error)
reject(error)
console.error("过滤学员用户失败:", error)
//
resolve(rows)
})
})
},

View File

@ -334,6 +334,13 @@ export default {
},
created() {
this.getList()
// action: 'edit' questionnaireId
const route = this.$route
if (route.query.action === 'edit' && route.query.questionnaireId) {
this.$nextTick(() => {
this.handleUpdateFromRoute(route.query.questionnaireId)
})
}
},
methods: {
/** 查询问卷列表 */
@ -395,10 +402,31 @@ export default {
handleUpdate(row) {
this.reset()
const questionnaireId = row.questionnaireId || this.ids[0]
this.handleUpdateFromRoute(questionnaireId)
},
/** 从路由参数或传入的ID打开编辑对话框 */
handleUpdateFromRoute(questionnaireId) {
if (!questionnaireId) {
return
}
this.reset()
getQuestionnaire(questionnaireId).then(response => {
this.form = response.data
this.open = true
this.title = "修改问卷"
//
if (this.$route.query.action === 'edit') {
const newQuery = { ...this.$route.query }
delete newQuery.action
delete newQuery.questionnaireId
this.$router.replace({
path: this.$route.path,
query: Object.keys(newQuery).length > 0 ? newQuery : {}
})
}
}).catch(error => {
console.error("获取问卷信息失败:", error)
this.$modal.msgError("获取问卷信息失败")
})
},
/** 提交按钮 */

View File

@ -50,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"
@ -154,6 +154,7 @@ export default {
generating: false,
reportDialogVisible: false,
comprehensiveReport: '',
// ========== ==========
OLLAMA_URL: 'http://192.168.0.106:11434/api/generate',
MODEL: 'deepseek-r1:32b'
}
@ -176,6 +177,7 @@ export default {
.then((res) => this.normalizeStudentOptions(res.data || []))
.catch(() => [])
let profilePromise = Promise.resolve([])
//
if (/^\d+$/.test(trimmed)) {
profilePromise = listProfile({
infoNumber: trimmed,
@ -184,10 +186,20 @@ export default {
})
.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]) => {
const merged = this.mergeUserOptions([...studentList, ...profileList])
// 使infoNumber
const merged = this.mergeUserOptions([...profileList, ...studentList])
this.cachedUserOptions = merged
return merged
})
@ -217,16 +229,46 @@ export default {
this.$message.warning('请输入姓名或信息编号')
return
}
this.fetchUserOptions(keyword).then((list) => {
//
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) {
this.$message.warning('未找到匹配的用户')
//
//
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) {
@ -263,6 +305,16 @@ export default {
}
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())
@ -512,6 +564,7 @@ ${typeLabel}${index + 1}${report.scaleName}
return `${SYSTEM_PROMPT}\n\n${userInfoText}\n\n${scaleReportsText}`
},
// OLLAMA API
async callOLLAMA(prompt) {
try {
const { data } = await axios.post(this.OLLAMA_URL, {
@ -536,13 +589,13 @@ ${typeLabel}${index + 1}${report.scaleName}
.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) {
@ -589,7 +642,7 @@ ${typeLabel}${index + 1}${report.scaleName}
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; text-align: right; color: #909399; font-size: 12px;">
<div>报告生成时间${parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}')}</div>
<div style="margin-top: 8px;">被评估人____________________</div>
<div style="margin-top: 8px;">被评估人<span style="color: #303133; font-weight: bold;">${userInfo.userName || '未知'}</span></div>
</div>
</div>
`

View File

@ -2,20 +2,38 @@
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="80px">
<el-form-item label="量表名称" prop="scaleName">
<el-input
<el-select
v-model="queryParams.scaleName"
placeholder="请输入量表名称"
placeholder="请选择或输入量表名称"
clearable
@keyup.enter.native="handleQuery"
filterable
allow-create
default-first-option
>
<el-option
v-for="name in uniqueScaleNames"
:key="name"
:label="name"
:value="name"
/>
</el-select>
</el-form-item>
<el-form-item label="量表编码" prop="scaleCode">
<el-input
<el-select
v-model="queryParams.scaleCode"
placeholder="请输入量表编码"
placeholder="请选择或输入量表编码"
clearable
@keyup.enter.native="handleQuery"
filterable
allow-create
default-first-option
>
<el-option
v-for="code in uniqueScaleCodes"
:key="code"
:label="code"
:value="code"
/>
</el-select>
</el-form-item>
<el-form-item label="量表类型" prop="scaleType">
<el-select v-model="queryParams.scaleType" placeholder="量表类型" clearable>
@ -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: {},
@ -524,10 +649,39 @@ export default {
{ 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: {
/** 获取字典标签 */
@ -536,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
@ -621,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()
})
}
@ -639,6 +810,7 @@ export default {
this.$modal.confirm('是否确认删除量表编号为"' + scaleIds + '"的数据项?').then(function() {
return delScale(scaleIds)
}).then(() => {
this.getAllScaleList() //
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
@ -790,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 || "导入失败")
@ -826,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 || "导入失败")
@ -858,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 || "导入失败")
@ -927,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) {
@ -937,6 +1166,7 @@ export default {
this.$modal.confirm('是否确认删除问卷"' + questionnaireName + '"的数据项?').then(() => {
return delQuestionnaire(questionnaireId)
}).then(() => {
this.getAllScaleList() //
this.getList()
this.$modal.msgSuccess("删除成功")
}).catch(() => {})

View File

@ -191,7 +191,23 @@ export default {
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
}
@ -209,6 +225,11 @@ export default {
this.createAssessment(test)
},
//
createNewAssessment(test) {
this.createAssessment(test)
},
//
createAssessment(test) {
this.loading = true