xinli/xinli-ui/src/views/psychology/report/comprehensive.vue
xiao12feng8 ff8cd67fdb refactor: 移除所有RuoYi相关痕迹,隐藏项目来源
- 重命名文件: ruoyi.js  common.js, ruoyi.scss  common.scss
- 重命名组件: RuoYi/  Common/
- 创建新类: XinliConfig.java (替代RuoYiConfig.java)
- 更新所有导入语句和引用 (50+ 处)
- 更新配置前缀: ruoyi  xinli
- 更新Swagger文档标题和描述
- 更新许可证版权信息
- 移除所有RuoYi文档链接和示例代码
2026-01-30 17:31:21 +08:00

1101 lines
43 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="comprehensive-assessment">
<el-card class="user-selector" shadow="never">
<el-form :inline="true" size="small">
<el-form-item label="选择用户">
<el-autocomplete
v-model="userSearchKeyword"
style="width: 320px"
clearable
:fetch-suggestions="searchUsers"
placeholder="输入姓名或信息编号搜索"
value-key="value"
:trigger-on-focus="false"
:debounce="400"
@select="handleUserSelect"
>
<template slot-scope="{ item }">
<div class="user-option">
<span class="name">{{ item.nickName || item.userName }}</span>
<span class="dept">{{ item.infoNumber ? `编号:${item.infoNumber}` : '' }}</span>
</div>
</template>
</el-autocomplete>
</el-form-item>
<el-form-item>
<el-button type="success" icon="el-icon-refresh" :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="220">
<template slot-scope="scope">
<el-tag
v-if="scope.row.sourceType === 'questionnaire'"
size="mini"
type="warning"
style="margin-right: 6px"
>问卷</el-tag>
<span>{{ scope.row.scaleName || '-' }}</span>
</template>
</el-table-column>
<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="success"
icon="el-icon-printer"
:disabled="!comprehensiveReport"
@click="printReport"
>打印报告</el-button>
<el-button
type="primary"
icon="el-icon-download"
:disabled="!comprehensiveReport"
@click="exportReport('word')"
>导出报告</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getUserAssessmentSummary, getStudentOptions, listAssessment } from '@/api/psychology/assessment'
import { getProfileByUserId, listProfile } from '@/api/psychology/profile'
import { getReport, listReport } from '@/api/psychology/report'
import { parseTime } from '@/utils/common'
import axios from 'axios'
export default {
name: 'ComprehensiveAssessment',
data() {
return {
selectedUserId: undefined,
userSearchKeyword: '',
userSearchLoading: false,
cachedUserOptions: [],
userProfile: null,
userSummary: null,
reportOptions: [],
selectedReports: [],
loading: false,
generating: false,
reportDialogVisible: false,
comprehensiveReport: '',
ragSourcesForReport: [], // RAG知识库来源
// ========== Ollama本地大模型配置服务器部署==========
API_URL: window.location.protocol === 'https:'
? '/ollama/api/chat'
: `http://${window.location.hostname}:11434/api/chat`,
API_KEY: '', // 本地模型不需要API Key
MODEL: 'deepseek-r1:32b'
// ========== 备用配置Kimi API - 本地开发)==========
// API_URL: 'https://api.moonshot.cn/v1/chat/completions',
// API_KEY: 'sk-U9fdriPxwBcrpWW0Ite3N0eVtX7VxnqqqYUIBAdWd1hgEA9m',
// MODEL: 'moonshot-v1-32k'
}
},
created() {
},
methods: {
searchUsers(query, cb) {
this.fetchUserOptions(query)
.then((list) => cb(list))
.catch(() => cb([]))
},
fetchUserOptions(keyword) {
const trimmed = (keyword || '').trim()
if (!trimmed) {
return Promise.resolve([])
}
this.userSearchLoading = true
const studentPromise = getStudentOptions({ keyword: trimmed, limit: 20 })
.then((res) => this.normalizeStudentOptions(res.data || []))
.catch(() => [])
let profilePromise = Promise.resolve([])
// 如果是纯数字,通过信息编号搜索
if (/^\d+$/.test(trimmed)) {
profilePromise = listProfile({
infoNumber: trimmed,
pageNum: 1,
pageSize: 20
})
.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]) => {
// 合并时优先使用档案数据因为档案数据包含完整的infoNumber
const merged = this.mergeUserOptions([...profileList, ...studentList])
this.cachedUserOptions = merged
return merged
})
.finally(() => {
this.userSearchLoading = false
})
},
buildUserLabel(option) {
if (!option) {
return ''
}
const name = option.nickName || option.userName || ''
const info = option.infoNumber ? `(编号:${option.infoNumber}` : ''
const dept = option.deptName ? ` - ${option.deptName}` : ''
return `${name}${info}${dept}`
},
handleUserSelect(option) {
if (!option || !option.userId) {
return
}
this.selectedUserId = option.userId
this.userSearchKeyword = this.buildUserLabel(option)
},
handleManualSearch() {
const keyword = (this.userSearchKeyword || '').trim()
if (!keyword) {
this.$message.warning('请输入姓名或信息编号')
return
}
// 从输入框中提取纯姓名或编号(去除括号、编号等格式)
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) {
// 不显示错误消息,让用户继续搜索或选择
// 如果用户已经选择了用户,保持选择状态
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) {
return list.map((item) => ({
userId: item.userId,
userName: item.userName,
nickName: item.nickName,
infoNumber: item.infoNumber,
deptName: item.deptName,
value: this.buildUserLabel(item)
}))
},
normalizeProfileOptions(rows) {
return rows
.filter((profile) => profile && profile.userId)
.map((profile) => ({
userId: profile.userId,
userName: profile.userName || profile.nickName,
nickName: profile.userName || profile.nickName,
infoNumber: profile.infoNumber,
deptName: profile.prisonArea || profile.deptName,
value: this.buildUserLabel({
userName: profile.userName || profile.nickName,
nickName: profile.userName || profile.nickName,
infoNumber: profile.infoNumber
})
}))
},
mergeUserOptions(list) {
const map = new Map()
list.forEach((item) => {
if (!item || !item.userId) {
return
}
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())
},
async loadUserData() {
if (!this.selectedUserId) {
this.$message.warning('请先选择用户')
return
}
this.loading = true
try {
// 直接加载该用户的所有测评记录(不使用汇总接口,避免被过滤)
const assessmentResponse = await listAssessment({
userId: this.selectedUserId,
status: '1', // 只加载已完成的测评
pageNum: 1,
pageSize: 1000
})
const assessments = assessmentResponse.rows || []
// 获取所有量表报告包含reportId
const scaleReportsResponse = await listReport({
userId: this.selectedUserId,
sourceType: 'assessment',
isGenerated: '1',
pageNum: 1,
pageSize: 1000
})
const reportMap = new Map()
if (scaleReportsResponse.rows) {
scaleReportsResponse.rows.forEach(report => {
if (report.assessmentId) {
reportMap.set(report.assessmentId, report)
}
})
}
// 转换为报告列表格式
const scaleReports = []
for (const assessment of assessments) {
// 检查是否有报告
if (assessment.hasReport) {
const report = reportMap.get(assessment.assessmentId)
scaleReports.push({
key: `${assessment.scaleId}-${assessment.assessmentId}`,
scaleId: assessment.scaleId,
scaleName: assessment.scaleName,
assessmentId: assessment.assessmentId,
reportId: report ? report.reportId : null,
reportTitle: report ? report.reportTitle : `${assessment.scaleName}测评报告`,
submitTime: assessment.submitTime || assessment.startTime,
totalScore: assessment.totalScore,
summary: report ? report.summary : '',
status: assessment.status,
sourceType: 'assessment'
})
}
}
// 获取用户基本信息(用于显示)
const summaryResponse = await getUserAssessmentSummary(this.selectedUserId)
this.userSummary = summaryResponse.data || null
// 加载问卷报告
const questionnaireReports = await this.loadQuestionnaireReports(this.selectedUserId)
// 合并量表和问卷报告
const combinedReports = [...scaleReports, ...questionnaireReports].sort((a, b) => {
const timeA = a.submitTime ? new Date(a.submitTime).getTime() : 0
const timeB = b.submitTime ? new Date(b.submitTime).getTime() : 0
return timeB - timeA
})
this.reportOptions = combinedReports
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.userSearchKeyword = ''
this.userProfile = null
this.userSummary = null
this.reportOptions = []
this.selectedReports = []
this.comprehensiveReport = ''
},
async loadQuestionnaireReports(userId) {
if (!userId) {
return []
}
try {
const response = await listReport({
userId: userId,
sourceType: 'questionnaire',
isGenerated: '1',
pageNum: 1,
pageSize: 1000
})
const rows = response.rows || []
return rows.map((row) => ({
key: `questionnaire-${row.reportId}`,
scaleId: row.sourceId,
scaleName: row.reportTitle || '问卷报告',
assessmentId: row.sourceId,
reportId: row.reportId,
reportTitle: row.reportTitle || '问卷报告',
submitTime: row.generateTime || row.createTime,
totalScore: row.totalScore || row.score || '-',
summary: row.summary || '',
status: row.isGenerated === '1' ? '1' : '0',
sourceType: 'questionnaire'
}))
} catch (error) {
console.error('加载问卷报告失败:', error)
return []
}
},
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
}
// 先保存选中的报告副本,防止在异步操作中被修改
const selectedReportsCopy = [...this.selectedReports]
console.log('generateComprehensiveReport - 选中报告数:', selectedReportsCopy.length, selectedReportsCopy)
this.generating = true
this.reportDialogVisible = true
this.comprehensiveReport = ''
try {
// 1. 获取选中量表的报告内容(传入副本)
const scaleReports = await this.fetchSelectedReports(selectedReportsCopy)
// 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(selectedReportsList) {
// 使用传入的列表,而不是 this.selectedReports
const reportsToProcess = selectedReportsList || this.selectedReports || []
console.log('fetchSelectedReports - 待处理报告数:', reportsToProcess.length, reportsToProcess)
const reports = []
for (const row of reportsToProcess) {
console.log('处理报告行:', row)
// 即使没有reportId也尝试添加基本信息
const sourceType = row.sourceType === 'questionnaire' ? 'questionnaire' : 'assessment'
let reportContent = ''
let summary = row.summary || ''
// 如果有reportId尝试获取详细内容
if (row.reportId) {
try {
const response = await getReport(row.reportId, sourceType)
if (response && response.data) {
reportContent = response.data.reportContent || ''
summary = response.data.summary || row.summary || ''
}
} catch (error) {
console.warn(`获取${sourceType === 'questionnaire' ? '问卷' : '量表'} ${row.scaleName} 的报告失败:`, error)
}
}
// 无论是否获取到详细内容,都添加到报告列表
reports.push({
scaleName: row.scaleName || row.reportTitle || '未知量表',
submitTime: row.submitTime,
totalScore: row.totalScore,
summary: summary,
content: reportContent,
sourceType
})
}
console.log('fetchSelectedReports - 返回报告数:', reports.length, reports)
return reports
},
buildUserInfoSummary() {
if (!this.userProfile) {
return {
userName: this.userSummary?.userName || this.userSummary?.nickName || '未知',
infoNumber: '-',
gender: '-',
age: '-',
birthday: '-',
nation: '-',
educationLevel: '-',
crimeName: '-',
sentenceTerm: '-',
sentenceStartDate: '-',
sentenceEndDate: '-',
entryDate: '-',
prisonArea: '-',
status: '-'
}
}
// 状态映射
const statusMap = { '0': '在押', '1': '释放', '2': '外出', '3': '假释' }
const genderMap = { '0': '男', '1': '女', '2': '未知' }
return {
userName: this.userProfile.userName || this.userSummary?.userName || '未知',
infoNumber: this.userProfile.infoNumber || '-',
gender: genderMap[this.userProfile.gender] || this.userProfile.gender || '-',
age: this.calculateAge(this.userProfile.birthday),
birthday: this.formatDate(this.userProfile.birthday) || '-',
nation: this.userProfile.nation || '-',
educationLevel: this.userProfile.educationLevel || '-',
crimeName: this.userProfile.crimeName || '-',
sentenceTerm: this.userProfile.sentenceTerm || '-',
sentenceStartDate: this.formatDate(this.userProfile.sentenceStartDate) || '-',
sentenceEndDate: this.formatDate(this.userProfile.sentenceEndDate) || '-',
entryDate: this.formatDate(this.userProfile.entryDate) || '-',
prisonArea: this.userProfile.prisonArea || '-',
status: statusMap[this.userProfile.status] || this.userProfile.status || '-'
}
},
buildAIPrompt(userInfo, scaleReports) {
// 构建量表清单
const scaleListText = scaleReports
.map((report, index) => {
const typeLabel = report.sourceType === 'questionnaire' ? '【问卷】' : '【量表】'
return `${index + 1}. ${typeLabel}${report.scaleName}(测评时间:${this.formatDateTime(report.submitTime)},得分:${report.totalScore}`
})
.join('\n')
const SYSTEM_PROMPT = [
'你是一名资深的监狱心理矫治专家和心理测评分析师,拥有丰富的罪犯心理评估和矫治工作经验。',
'请根据提供的服刑人员个人信息和多个心理量表/问卷测评结果,生成一份专业、详细、深入的综合心理评估报告。',
'',
'【重要提示】本次分析涉及以下量表/问卷,请在报告中对每一个进行深入分析:',
scaleListText,
'',
'【报告撰写要求】',
'',
'一、报告字数要求:',
' - 整份报告总字数不少于3000字',
' - 每个量表的分析不少于400字',
' - 综合评估结论不少于800字',
' - 管控措施建议不少于600字',
'',
'二、个人信息深度分析要求:',
' - 必须结合被测试者的所有个人信息字段(姓名、年龄、性别、民族、文化程度、罪名、刑期、入监时间、监区、当前状态等)进行深入分析',
' - 分析年龄与心理特征的关系(如青年期冲动性、中年期稳定性等)',
' - 分析文化程度对认知能力和心理调适能力的影响',
' - 分析罪名类型反映的人格特征和行为模式(如暴力犯罪、经济犯罪、毒品犯罪等不同类型的心理特点)',
' - 分析刑期长短对心理状态的影响(短刑期焦虑、长刑期适应等)',
' - 分析服刑阶段(入监初期、服刑中期、临释期)的心理特点',
'',
'三、量表因子深度分析要求:',
' - 对每个量表的各个因子进行逐一分析,说明因子得分的心理学含义',
' - 结合该量表的心理学理论背景进行专业解读',
' - 说明该量表的常模参照标准和得分等级划分',
' - 分析各因子之间的相互关系和整体心理画像',
' - 将量表结果与被测者的个人背景信息进行关联分析',
'',
'四、心理学知识结合要求:',
' - 引用相关心理学理论(如人格理论、认知行为理论、社会学习理论等)',
' - 结合犯罪心理学知识分析犯罪行为的心理成因',
' - 运用发展心理学知识分析年龄阶段特点',
' - 应用临床心理学知识评估心理健康状况',
'',
'五、管控措施要求(必须结合监狱工作实际):',
' - 提出具体的日常管理建议(如监舍安排、劳动岗位、活动参与等)',
' - 制定针对性的心理矫治方案(个别谈话、团体辅导、心理咨询等)',
' - 提出重点关注事项和风险防控措施',
' - 建议适合的教育改造项目和技能培训',
' - 提出与家属沟通和社会支持方面的建议',
' - 针对不同风险等级提出分级管控策略',
' - 建议定期心理复评的时间和重点关注指标',
'',
'六、报告格式要求:',
' - 使用HTML格式输出',
' - 使用<h3>标签作为章节标题',
' - 使用<h4>标签作为小节标题',
' - 使用<p>标签作为段落',
' - 使用<ul><li>标签列出要点',
' - 使用<strong>标签强调重要内容',
'',
'七、报告结构要求:',
' 1. 综合概述约300字列出所有分析的量表名称概述评估目的和方法',
' 2. 个人背景分析约400字深入分析被测者的个人信息与心理特征的关系',
' 3. 各量表详细分析每个量表约400字',
' - 量表简介和测评目的',
' - 各因子得分分析',
' - 心理学理论解读',
' - 与个人背景的关联分析',
' 4. 综合评估结论约800字',
' - 整体心理状态评估',
' - 主要心理问题识别',
' - 心理风险等级判定',
' - 人格特征总结',
' 5. 管控措施与矫治建议约600字',
' - 日常管理建议',
' - 心理矫治方案',
' - 风险防控措施',
' - 教育改造建议',
' - 后续跟踪计划',
'',
'八、其他要求:',
' - 语言专业、客观、严谨',
' - 仅输出分析结果,不要包含思考过程、<think>标签或</think>标签',
' - 不要使用markdown格式只使用HTML标签'
].join('\n')
const userInfoText = `
【被测试者完整个人信息】
- 姓名:${userInfo.userName}
- 信息编号:${userInfo.infoNumber}
- 性别:${userInfo.gender}
- 年龄:${userInfo.age}
- 出生日期:${userInfo.birthday}
- 民族:${userInfo.nation}
- 文化程度:${userInfo.educationLevel}
- 罪名:${userInfo.crimeName}
- 刑期:${userInfo.sentenceTerm}
- 刑期起日:${userInfo.sentenceStartDate}
- 刑期止日:${userInfo.sentenceEndDate}
- 入监时间:${userInfo.entryDate}
- 所在监区:${userInfo.prisonArea}
- 当前状态:${userInfo.status}
`.trim()
const scaleReportsText = scaleReports
.map((report, index) => {
const typeLabel = report.sourceType === 'questionnaire' ? '问卷' : '量表'
// 提取完整内容以便AI更好地分析
const contentText = report.content
.replace(/<[^>]*>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/\s+/g, ' ')
.substring(0, 2000) // 增加内容长度以获取更多因子信息
return `
【${typeLabel}${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【各量表/问卷详细报告及因子分析】\n${scaleReportsText}`
},
// Ollama 本地大模型调用方法
async callOLLAMA(prompt) {
try {
// 1. 先调用RAG服务获取知识库上下文
let knowledgeContext = '';
let ragSources = [];
const RAG_API_URL = window.location.protocol === 'https:'
? '/rag-api/api/rag-analyze'
: `http://${window.location.hostname}:5000/api/rag-analyze`;
try {
// 从prompt中提取关键信息用于RAG检索
const ragResponse = await axios.post(RAG_API_URL, {
reportContent: prompt,
reportTitle: '综合心理评估'
}, { timeout: 10000 });
if (ragResponse.data && ragResponse.data.success) {
knowledgeContext = ragResponse.data.data.knowledgeContext || '';
ragSources = ragResponse.data.data.sources || [];
// ========== 在控制台打印完整的知识库引用信息 ==========
console.log('========================================');
console.log('📚 RAG知识库检索结果');
console.log('========================================');
console.log('检索到的文档数量:', ragSources.length);
console.log('');
console.log('📋 引用的知识库文件列表:');
ragSources.forEach((source, index) => {
console.log(` ${index + 1}. 文件名: ${source.filename || '未知'}`);
console.log(` 相似度: ${source.similarity ? (source.similarity * 100).toFixed(2) + '%' : '未知'}`);
console.log(` 内容预览: ${source.content ? source.content.substring(0, 100) + '...' : '无内容'}`);
console.log('');
});
console.log('========================================');
console.log('📝 知识库上下文长度:', knowledgeContext.length, '字符');
console.log('========================================');
// ========== 打印结束 ==========
}
} catch (ragErr) {
console.warn('RAG服务调用失败将不使用知识库增强:', ragErr.message);
}
// 2. 如果有知识库上下文添加到prompt中
let enhancedPrompt = prompt;
if (knowledgeContext) {
enhancedPrompt = prompt + '\n\n【专业知识库参考资料】\n' + knowledgeContext + '\n\n请结合以上专业心理学资料进行分析使报告更加专业和有深度。';
}
// 3. 调用AI API根据API类型选择不同的请求格式
let response = '';
if (this.API_URL.includes('11434')) {
// Ollama 本地模型格式
const { data } = await axios.post(this.API_URL, {
model: this.MODEL,
messages: [
{ role: 'user', content: enhancedPrompt }
],
stream: false,
options: {
temperature: 0.3,
num_predict: 8000
}
}, {
headers: {
'Content-Type': 'application/json'
},
timeout: 600000 // Ollama本地模型需要更长时间设置10分钟
});
response = data?.message?.content ?? '';
console.log('Ollama响应:', response);
} else {
// OpenAI兼容格式Kimi等
const { data } = await axios.post(this.API_URL, {
model: this.MODEL,
messages: [
{ role: 'user', content: enhancedPrompt }
],
temperature: 0.3,
max_tokens: 8000,
stream: false
}, {
headers: {
'Authorization': `Bearer ${this.API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 180000
});
response = data?.choices?.[0]?.message?.content ?? '';
console.log('OpenAI格式响应:', response);
}
// 4. 如果有知识库来源,添加参考资料说明
if (ragSources && ragSources.length > 0) {
this.ragSourcesForReport = ragSources;
}
// 清理响应内容
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()
console.log('清理后响应:', response)
if (!response) {
throw new Error('AI分析返回结果为空')
}
return response
} catch (error) {
console.error('AI分析失败:', error)
throw new Error('AI分析失败' + (error.message || '未知错误'))
}
},
formatReport(aiReport, userInfo, scaleReports) {
console.log('formatReport - scaleReports:', scaleReports)
console.log('formatReport - scaleReports length:', scaleReports ? scaleReports.length : 0)
// 先构建量表列表HTML
let scaleListHtml = ''
if (scaleReports && scaleReports.length > 0) {
const scaleItems = scaleReports.map((r, i) => {
const typeLabel = r.sourceType === 'questionnaire' ? '【问卷】' : '【量表】'
const timeStr = this.formatDateTime(r.submitTime)
return `<li>${i + 1}. ${typeLabel}${r.scaleName}(测评时间:${timeStr},总分:${r.totalScore}</li>`
})
scaleListHtml = scaleItems.join('')
} else {
scaleListHtml = '<li>暂无量表数据</li>'
}
// 构建完整的报告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: 120px; background-color: #fafafa;"><strong>姓名</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.userName}</td>
<td style="padding: 8px; border: 1px solid #ddd; width: 120px; background-color: #fafafa;"><strong>信息编号</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.infoNumber}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd; background-color: #fafafa;"><strong>性别</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.gender}</td>
<td style="padding: 8px; border: 1px solid #ddd; background-color: #fafafa;"><strong>年龄</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.age}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd; background-color: #fafafa;"><strong>民族</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.nation}</td>
<td style="padding: 8px; border: 1px solid #ddd; background-color: #fafafa;"><strong>文化程度</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.educationLevel}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd; background-color: #fafafa;"><strong>罪名</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;" colspan="3">${userInfo.crimeName}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd; background-color: #fafafa;"><strong>刑期</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.sentenceTerm}</td>
<td style="padding: 8px; border: 1px solid #ddd; background-color: #fafafa;"><strong>当前状态</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.status}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd; background-color: #fafafa;"><strong>刑期起日</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.sentenceStartDate}</td>
<td style="padding: 8px; border: 1px solid #ddd; background-color: #fafafa;"><strong>刑期止日</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.sentenceEndDate}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd; background-color: #fafafa;"><strong>入监时间</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.entryDate}</td>
<td style="padding: 8px; border: 1px solid #ddd; background-color: #fafafa;"><strong>所在监区</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.prisonArea}</td>
</tr>
</table>
</div>
<div class="scale-summary" style="margin-bottom: 30px;">
<h2>参与测评的量表/问卷(共${scaleReports ? scaleReports.length : 0}个)</h2>
<ul style="line-height: 2; padding-left: 20px;">
${scaleListHtml}
</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;">
<div>报告生成时间:${parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}')}</div>
<div style="margin-top: 8px;">被评估人:<span style="color: #303133; font-weight: bold; text-decoration: underline;">______________________</span></div>
</div>
<div style="margin-top: 30px; padding: 15px; background-color: #FDF6EC; border: 1px solid #E6A23C; border-radius: 4px; text-align: center;">
<p style="margin: 0; color: #E6A23C; font-size: 14px; font-weight: bold;">
⚠️ 此结果仅供参考,不可作为临床诊断的唯一标准
</p>
</div>
${this.ragSourcesForReport && this.ragSourcesForReport.length > 0 ? `
<div class="rag-sources" style="margin-top: 20px; padding: 15px; background: #f5f7fa; border-radius: 8px;">
<h4 style="margin: 0 0 10px 0; color: #409EFF;"><i class="el-icon-document"></i> 参考知识库资料</h4>
<ul style="margin: 0; padding-left: 20px; color: #666;">
${this.ragSourcesForReport.map(source => `<li style="margin-bottom: 5px;"><strong>${source.filename}</strong></li>`).join('')}
</ul>
</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
},
buildReportHtml() {
return `
<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>
`
},
exportReport(format) {
if (!this.comprehensiveReport) {
this.$message.warning('报告内容为空')
return
}
const reportHtml = this.buildReportHtml()
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 {
this.printReport()
}
},
printReport() {
if (!this.comprehensiveReport) {
this.$message.warning('报告内容为空')
return
}
const reportHtml = this.buildReportHtml()
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>