综合报告

This commit is contained in:
green 2025-11-26 14:23:53 +08:00
parent 343cb8c76d
commit 180ccde18b
5 changed files with 801 additions and 2 deletions

View File

@ -6,7 +6,7 @@ spring:
druid:
# 主库数据源
master:
url: jdbc:mysql://127.0.0.1:3306/ry_xinli?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
url: jdbc:mysql://1.15.149.240:3306/ry_xinli?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: ry_xinli
password: ZLZBcfGtsWJe5r4z
# 从库数据源

View File

@ -0,0 +1,190 @@
-- 添加综合评估菜单
-- 菜单类型C菜单
-- 父菜单ID2009心理测评管理
-- 排序在报告管理之后设置为14在主观题评分order_num=13之后
-- 设置字符集(确保中文正确显示)
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!50503 SET NAMES utf8mb4 */;
-- 添加综合评估菜单(菜单项)
INSERT INTO `sys_menu` (
`menu_name`,
`parent_id`,
`order_num`,
`path`,
`component`,
`query`,
`route_name`,
`is_frame`,
`is_cache`,
`menu_type`,
`visible`,
`status`,
`perms`,
`icon`,
`create_by`,
`create_time`,
`remark`
) VALUES (
'综合评估', -- 菜单名称
2009, -- 父菜单ID心理测评管理
14, -- 显示顺序在主观题评分order_num=13之后
'report/comprehensive', -- 路由地址
'psychology/report/comprehensive', -- 组件路径
NULL, -- 路由参数
'ComprehensiveAssessment', -- 路由名称
1, -- 是否为外链1否
0, -- 是否缓存0缓存
'C', -- 菜单类型C菜单
'0', -- 菜单状态0显示
'0', -- 菜单状态0正常
'psychology:report:comprehensive', -- 权限标识
'chart', -- 菜单图标
'admin', -- 创建者
NOW(), -- 创建时间
'综合评估菜单' -- 备注
);
-- 添加综合评估的按钮权限
-- 综合评估查询
INSERT INTO `sys_menu` (
`menu_name`,
`parent_id`,
`order_num`,
`path`,
`component`,
`query`,
`route_name`,
`is_frame`,
`is_cache`,
`menu_type`,
`visible`,
`status`,
`perms`,
`icon`,
`create_by`,
`create_time`,
`remark`
)
SELECT
'综合评估查询',
menu_id,
1,
'',
'',
NULL,
'',
1,
0,
'F',
'0',
'0',
'psychology:report:comprehensive:query',
'#',
'admin',
NOW(),
''
FROM sys_menu
WHERE menu_name = '综合评估' AND parent_id = 2009
LIMIT 1;
-- 综合评估生成
INSERT INTO `sys_menu` (
`menu_name`,
`parent_id`,
`order_num`,
`path`,
`component`,
`query`,
`route_name`,
`is_frame`,
`is_cache`,
`menu_type`,
`visible`,
`status`,
`perms`,
`icon`,
`create_by`,
`create_time`,
`remark`
)
SELECT
'综合评估生成',
menu_id,
2,
'',
'',
NULL,
'',
1,
0,
'F',
'0',
'0',
'psychology:report:comprehensive:generate',
'#',
'admin',
NOW(),
''
FROM sys_menu
WHERE menu_name = '综合评估' AND parent_id = 2009
LIMIT 1;
-- 综合评估导出
INSERT INTO `sys_menu` (
`menu_name`,
`parent_id`,
`order_num`,
`path`,
`component`,
`query`,
`route_name`,
`is_frame`,
`is_cache`,
`menu_type`,
`visible`,
`status`,
`perms`,
`icon`,
`create_by`,
`create_time`,
`remark`
)
SELECT
'综合评估导出',
menu_id,
3,
'',
'',
NULL,
'',
1,
0,
'F',
'0',
'0',
'psychology:report:comprehensive:export',
'#',
'admin',
NOW(),
''
FROM sys_menu
WHERE menu_name = '综合评估' AND parent_id = 2009
LIMIT 1;
-- 为管理员角色role_id=1添加菜单权限
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, menu_id FROM sys_menu WHERE menu_name = '综合评估' AND parent_id = 2009
ON DUPLICATE KEY UPDATE role_id = role_id;
-- 为管理员角色添加按钮权限
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT 1, m2.menu_id
FROM sys_menu m1
INNER JOIN sys_menu m2 ON m2.parent_id = m1.menu_id
WHERE m1.menu_name = '综合评估' AND m1.parent_id = 2009
ON DUPLICATE KEY UPDATE role_id = role_id;

View File

@ -347,6 +347,17 @@ export const dynamicRoutes = [
roles: ['admin']
}
},
// 综合评估
{
path: 'report/comprehensive',
name: 'ComprehensiveAssessment',
component: () => import('@/views/psychology/report/comprehensive'),
meta: {
title: '综合评估',
icon: 'chart',
roles: ['admin']
}
},
// 量表权限管理
{
path: 'permission',

View File

@ -521,7 +521,6 @@ export default {
// 使101
resolve(101);
});
>>>>>>> 13bbd8b9e742b8b2a020195d81f66187af7d4066
});
},
//

View File

@ -0,0 +1,599 @@
<template>
<div class="comprehensive-assessment">
<el-card class="user-selector" shadow="never">
<el-form :inline="true" size="small">
<el-form-item label="选择用户">
<el-select
v-model="selectedUserId"
style="width: 280px"
clearable
filterable
remote
reserve-keyword
:remote-method="searchUsers"
:loading="userOptionsLoading"
placeholder="输入姓名或账号搜索"
@change="handleUserChange"
>
<el-option
v-for="item in userOptions"
:key="item.userId"
:label="buildUserLabel(item)"
:value="item.userId"
>
<div class="user-option">
<span class="name">{{ item.nickName || item.userName }}</span>
<span class="dept">{{ item.deptName || '未分配单位' }}</span>
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" :disabled="!selectedUserId" @click="loadUserData">
载入
</el-button>
<el-button icon="el-icon-refresh" @click="resetSelection">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card v-if="userProfile" class="user-info" shadow="never">
<el-descriptions :column="3" border size="small">
<el-descriptions-item label="姓名">{{ userProfile.userName || '-' }}</el-descriptions-item>
<el-descriptions-item label="年龄">{{ calculateAge(userProfile.birthday) || '-' }}</el-descriptions-item>
<el-descriptions-item label="信息编号">{{ userProfile.infoNumber || '-' }}</el-descriptions-item>
<el-descriptions-item label="罪名">{{ userProfile.crimeName || '-' }}</el-descriptions-item>
<el-descriptions-item label="刑期">{{ userProfile.sentenceTerm || '-' }}</el-descriptions-item>
<el-descriptions-item label="刑期起日">{{ formatDate(userProfile.sentenceStartDate) || '-' }}</el-descriptions-item>
<el-descriptions-item label="刑期止日">{{ formatDate(userProfile.sentenceEndDate) || '-' }}</el-descriptions-item>
<el-descriptions-item label="文化程度">{{ userProfile.educationLevel || '-' }}</el-descriptions-item>
<el-descriptions-item label="民族">{{ userProfile.nation || '-' }}</el-descriptions-item>
</el-descriptions>
</el-card>
<el-card class="scale-list" shadow="never" v-loading="loading">
<div slot="header">
<span>量表列表请勾选需要分析的量表</span>
<div class="header-actions">
<el-button
type="primary"
plain
icon="el-icon-magic-stick"
size="mini"
:disabled="selectedReports.length === 0 || generating"
:loading="generating"
@click="generateComprehensiveReport"
>
AI生成综合报告
</el-button>
</div>
</div>
<el-table
ref="reportTable"
:data="reportOptions"
@selection-change="handleReportSelection"
border
empty-text="暂无可用的测评报告"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="scaleName" label="量表名称" min-width="200" />
<el-table-column prop="reportTitle" label="报告标题" min-width="240" :show-overflow-tooltip="true" />
<el-table-column label="测评时间" width="200" align="center">
<template slot-scope="scope">
{{ formatDateTime(scope.row.submitTime) }}
</template>
</el-table-column>
<el-table-column label="得分" width="100" align="center">
<template slot-scope="scope">
{{ scope.row.totalScore != null ? scope.row.totalScore : '-' }}
</template>
</el-table-column>
</el-table>
<el-empty
v-if="!loading && reportOptions.length === 0"
description="该用户暂无可用的量表报告"
></el-empty>
</el-card>
<el-dialog
title="综合评估报告"
:visible.sync="reportDialogVisible"
width="80%"
:close-on-click-modal="false"
append-to-body
>
<div v-loading="generating" class="report-preview">
<div v-if="comprehensiveReport" v-html="comprehensiveReport" class="report-content"></div>
<div v-else class="empty-report">报告生成中请稍候...</div>
</div>
<div slot="footer">
<el-button
type="primary"
icon="el-icon-download"
:disabled="!comprehensiveReport"
@click="exportReport('word')"
>导出报告</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getUserAssessmentSummary, getStudentOptions } from '@/api/psychology/assessment'
import { getProfileByUserId } from '@/api/psychology/profile'
import { getReport } from '@/api/psychology/report'
import { parseTime } from '@/utils/ruoyi'
import axios from 'axios'
export default {
name: 'ComprehensiveAssessment',
data() {
return {
selectedUserId: undefined,
userOptions: [],
userOptionsLoading: false,
userProfile: null,
userSummary: null,
reportOptions: [],
selectedReports: [],
loading: false,
generating: false,
reportDialogVisible: false,
comprehensiveReport: '',
OLLAMA_URL: 'http://192.168.0.106:11434/api/generate',
MODEL: 'deepseek-r1:32b'
}
},
created() {
this.searchUsers('')
},
methods: {
searchUsers(query) {
this.userOptionsLoading = true
getStudentOptions({ keyword: query, limit: 20 })
.then((res) => {
this.userOptions = res.data || []
})
.finally(() => {
this.userOptionsLoading = false
})
},
buildUserLabel(option) {
if (!option) {
return ''
}
const name = option.nickName || option.userName || ''
const dept = option.deptName ? ` - ${option.deptName}` : ''
return name + dept
},
handleUserChange() {
if (!this.selectedUserId) {
this.resetSelection()
}
},
async loadUserData() {
if (!this.selectedUserId) {
this.$message.warning('请先选择用户')
return
}
this.loading = true
try {
//
const summaryResponse = await getUserAssessmentSummary(this.selectedUserId)
this.userSummary = summaryResponse.data || null
//
const rawScales = (this.userSummary && Array.isArray(this.userSummary.scales)) ? this.userSummary.scales : []
this.reportOptions = rawScales.reduce((result, scale) => {
const attempts = Array.isArray(scale.attempts) ? scale.attempts : []
const reportRows = attempts
.filter((attempt) => attempt && attempt.reportId)
.map((attempt) => ({
key: `${scale.scaleId}-${attempt.assessmentId}`,
scaleId: scale.scaleId,
scaleName: scale.scaleName,
assessmentId: attempt.assessmentId,
reportId: attempt.reportId,
reportTitle: attempt.reportTitle || `${scale.scaleName}测评报告`,
submitTime: attempt.submitTime || attempt.startTime,
totalScore: attempt.totalScore,
summary: attempt.reportSummary || '',
status: attempt.status
}))
return result.concat(reportRows)
}, [])
this.selectedReports = []
this.$nextTick(() => {
if (this.$refs.reportTable && this.$refs.reportTable.doLayout) {
this.$refs.reportTable.doLayout()
}
})
//
try {
const profileResponse = await getProfileByUserId(this.selectedUserId)
this.userProfile = profileResponse.data || null
} catch (error) {
console.warn('获取用户档案失败:', error)
this.userProfile = null
}
} catch (error) {
console.error('加载用户数据失败:', error)
this.$message.error('加载用户数据失败')
} finally {
this.loading = false
}
},
resetSelection() {
this.selectedUserId = undefined
this.userProfile = null
this.userSummary = null
this.reportOptions = []
this.selectedReports = []
this.comprehensiveReport = ''
},
formatDateTime(value) {
if (!value) return '-'
return parseTime(value)
},
formatDate(value) {
if (!value) return '-'
if (typeof value === 'string') return value
return parseTime(value, '{y}-{m}-{d}')
},
calculateAge(birthday) {
if (!birthday) return '-'
const birth = new Date(birthday)
const today = new Date()
let age = today.getFullYear() - birth.getFullYear()
const monthDiff = today.getMonth() - birth.getMonth()
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
age--
}
return age + '岁'
},
handleReportSelection(selection) {
this.selectedReports = selection
},
async generateComprehensiveReport() {
if (this.selectedReports.length === 0) {
this.$message.warning('请至少选择一个报告')
return
}
this.generating = true
this.reportDialogVisible = true
this.comprehensiveReport = ''
try {
// 1.
const scaleReports = await this.fetchSelectedReports()
// 2.
const userInfoSummary = this.buildUserInfoSummary()
// 3. AI
const prompt = this.buildAIPrompt(userInfoSummary, scaleReports)
// 4. AI
const aiReport = await this.callOLLAMA(prompt)
// 5.
this.comprehensiveReport = this.formatReport(aiReport, userInfoSummary, scaleReports)
} catch (error) {
console.error('生成报告失败:', error)
this.$message.error('生成报告失败:' + (error.message || '未知错误'))
} finally {
this.generating = false
}
},
async fetchSelectedReports() {
const reports = []
for (const row of this.selectedReports) {
if (!row.reportId) {
continue
}
try {
const response = await getReport(row.reportId, 'assessment')
if (response && response.data) {
reports.push({
scaleName: row.scaleName,
submitTime: row.submitTime,
totalScore: row.totalScore,
summary: response.data.summary || row.summary || '',
content: response.data.reportContent || ''
})
}
} catch (error) {
console.warn(`获取量表 ${row.scaleName} 的报告失败:`, error)
}
}
return reports
},
buildUserInfoSummary() {
if (!this.userProfile) {
return {
userName: this.userSummary?.userName || this.userSummary?.nickName || '未知',
age: '-',
crimeName: '-',
sentenceTerm: '-',
sentenceEndDate: '-',
educationLevel: '-',
nation: '-'
}
}
return {
userName: this.userProfile.userName || this.userSummary?.userName || '未知',
age: this.calculateAge(this.userProfile.birthday),
crimeName: this.userProfile.crimeName || '-',
sentenceTerm: this.userProfile.sentenceTerm || '-',
sentenceEndDate: this.formatDate(this.userProfile.sentenceEndDate),
educationLevel: this.userProfile.educationLevel || '-',
nation: this.userProfile.nation || '-'
}
},
buildAIPrompt(userInfo, scaleReports) {
const SYSTEM_PROMPT = [
'你是专业心理测评综合评估分析师。请根据提供的用户信息和多个量表测评结果,生成一份综合评估报告。',
'要求:',
'1. 综合分析用户的心理状态,结合年龄、罪名、刑期等背景信息;',
'2. 对比分析所选量表的测评结果,找出共同点和差异点;',
'3. 提供专业的综合评估结论800-1200字',
'4. 使用HTML格式输出包含综合概述、量表结果分析、综合评估、建议措施等部分',
'5. 使用<h3>标签作为小标题,<p>标签作为段落;',
'6. 仅输出分析结果,不要包含思考过程、<think>标签或</think>标签。'
].join('\n')
const userInfoText = `
用户基本信息
- 姓名${userInfo.userName}
- 年龄${userInfo.age}
- 罪名${userInfo.crimeName}
- 刑期${userInfo.sentenceTerm}
- 刑期止日${userInfo.sentenceEndDate}
- 文化程度${userInfo.educationLevel}
- 民族${userInfo.nation}
`.trim()
const scaleReportsText = scaleReports
.map((report, index) => {
const contentText = report.content
.replace(/<[^>]*>/g, '')
.replace(/&nbsp;/g, ' ')
.substring(0, 500)
return `
量表${index + 1}${report.scaleName}
测评时间${this.formatDateTime(report.submitTime)}
总分${report.totalScore}
报告摘要${report.summary || '无'}
报告内容${contentText}...
`.trim()
})
.join('\n\n')
return `${SYSTEM_PROMPT}\n\n${userInfoText}\n\n${scaleReportsText}`
},
async callOLLAMA(prompt) {
try {
const { data } = await axios.post(this.OLLAMA_URL, {
model: this.MODEL,
prompt: prompt,
temperature: 0.2,
num_predict: 2000,
stream: false
}, {
timeout: 120000
})
let response = data?.response ?? ''
//
response = response
.replace(/<think>[\s\S]*?<\/think>/gi, '')
.replace(/<think>[\s\S]*?<\/redacted_reasoning>/gi, '')
.replace(/```html\s*/gi, '')
.replace(/```\s*/g, '')
.replace(/```[a-z]*\s*/gi, '')
.trim()
if (!response) {
throw new Error('AI分析返回结果为空')
}
return response
} catch (error) {
console.error('AI分析失败:', error)
throw new Error('AI分析失败' + (error.message || '未知错误'))
}
},
formatReport(aiReport, userInfo, scaleReports) {
// HTML
const reportHtml = `
<div class="comprehensive-report">
<h1 style="text-align: center; margin-bottom: 30px;">综合评估报告</h1>
<div class="user-info-section" style="margin-bottom: 30px; padding: 15px; background-color: #f5f7fa; border-radius: 4px;">
<h2 style="margin-top: 0;">用户基本信息</h2>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px; border: 1px solid #ddd; width: 150px;"><strong>姓名</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.userName}</td>
<td style="padding: 8px; border: 1px solid #ddd; width: 150px;"><strong>年龄</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.age}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>罪名</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.crimeName}</td>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>刑期</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.sentenceTerm}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>刑期止日</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.sentenceEndDate}</td>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>文化程度</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.educationLevel}</td>
</tr>
</table>
</div>
<div class="scale-summary" style="margin-bottom: 30px;">
<h2>参与测评的量表</h2>
<ul>
${scaleReports.map((r, i) => `<li>${i + 1}. ${r.scaleName}(测评时间:${this.formatDateTime(r.submitTime)},总分:${r.totalScore}</li>`).join('')}
</ul>
</div>
<div class="ai-analysis">
<h2 style="color: #67C23A; border-bottom: 2px solid #67C23A; padding-bottom: 10px;">AI综合评估分析</h2>
${this.formatAIResult(aiReport)}
</div>
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; text-align: right; color: #909399; font-size: 12px;">
报告生成时间${parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}')}
</div>
</div>
`
return reportHtml
},
formatAIResult(text) {
let html = text.trim()
// HTML
if (html.includes('<h3>') || html.includes('<p>') || html.includes('<div>')) {
return html
}
//
html = html.replace(/^(\d+[\.、]?\s*[^\n]+)$/gm, '<h3>$1</h3>')
html = html.replace(/^([^\n]*(?:结论|分析|建议|总结|概述)[^\n]*)$/gm, '<h3>$1</h3>')
// <p>
html = html.split('\n\n').map((para) => {
para = para.trim()
if (!para) return ''
if (para.startsWith('<h3>')) return para
return '<p>' + para.replace(/\n/g, '<br>') + '</p>'
}).join('')
return html
},
exportReport(format) {
if (!this.comprehensiveReport) {
this.$message.warning('报告内容为空')
return
}
const reportHtml = `
<html>
<head>
<meta charset="UTF-8" />
<title>综合评估报告</title>
<style>
body { font-family: 'Microsoft Yahei', sans-serif; padding: 32px; color: #303133; }
h1, h2, h3 { margin: 16px 0; }
h1 { text-align: center; font-size: 24px; }
h2 { font-size: 20px; color: #1f2d3d; }
h3 { font-size: 18px; color: #606266; }
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
table td, table th { border: 1px solid #ddd; padding: 8px; }
ul { line-height: 1.8; }
p { line-height: 1.8; margin: 10px 0; }
</style>
</head>
<body>
${this.comprehensiveReport}
</body>
</html>
`
if (format === 'word') {
const blob = new Blob(['\ufeff', reportHtml], { type: 'application/msword' })
const filename = `综合评估报告_${this.userProfile?.userName || '用户'}_${Date.now()}.doc`
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(link.href)
this.$message.success('导出成功')
} else {
const printWindow = window.open('', '_blank')
if (!printWindow) {
this.$message.error('无法打开打印窗口,请检查浏览器是否阻止了弹窗')
return
}
printWindow.document.write(reportHtml)
printWindow.document.close()
printWindow.focus()
printWindow.print()
}
}
}
}
</script>
<style scoped>
.comprehensive-assessment {
padding: 20px;
}
.user-selector {
margin-bottom: 20px;
}
.user-info {
margin-bottom: 20px;
}
.scale-list {
margin-bottom: 20px;
}
.header-actions {
display: flex;
justify-content: flex-end;
}
.user-option {
display: flex;
justify-content: space-between;
}
.user-option .name {
font-weight: 500;
}
.user-option .dept {
color: #909399;
font-size: 12px;
}
.report-preview {
min-height: 400px;
max-height: 600px;
overflow-y: auto;
}
.report-content {
line-height: 1.8;
}
.report-content h3 {
color: #409EFF;
margin-top: 20px;
margin-bottom: 10px;
}
.report-content p {
margin: 10px 0;
text-indent: 2em;
}
.empty-report {
text-align: center;
padding: 100px 0;
color: #909399;
}
</style>