xinli/xinli-ui/src/views/psychology/report/comprehensive.vue

1101 lines
43 KiB
Vue
Raw Normal View History

2025-11-26 14:23:53 +08:00
<template>
<div class="comprehensive-assessment">
<el-card class="user-selector" shadow="never">
<el-form :inline="true" size="small">
<el-form-item label="选择用户">
2025-11-27 18:12:23 +08:00
<el-autocomplete
v-model="userSearchKeyword"
style="width: 320px"
2025-11-26 14:23:53 +08:00
clearable
2025-11-27 18:12:23 +08:00
:fetch-suggestions="searchUsers"
placeholder="输入姓名或信息编号搜索"
value-key="value"
:trigger-on-focus="false"
:debounce="400"
@select="handleUserSelect"
2025-11-26 14:23:53 +08:00
>
2025-11-27 18:12:23 +08:00
<template slot-scope="{ item }">
2025-11-26 14:23:53 +08:00
<div class="user-option">
<span class="name">{{ item.nickName || item.userName }}</span>
2025-11-27 18:12:23 +08:00
<span class="dept">{{ item.infoNumber ? `编号:${item.infoNumber}` : '' }}</span>
2025-11-26 14:23:53 +08:00
</div>
2025-11-27 18:12:23 +08:00
</template>
</el-autocomplete>
2025-11-26 14:23:53 +08:00
</el-form-item>
<el-form-item>
2025-11-27 18:12:23 +08:00
<el-button type="success" icon="el-icon-refresh" :disabled="!selectedUserId" @click="loadUserData">
2025-11-26 14:23:53 +08:00
载入
</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">
2025-11-28 17:31:13 +08:00
<span>量表/问卷列表请勾选需要分析的量表/问卷</span>
2025-11-26 14:23:53 +08:00
<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" />
2025-11-27 18:12:23 +08:00
<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>
2025-11-26 14:23:53 +08:00
<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">
2025-11-27 18:12:23 +08:00
<el-button
type="success"
icon="el-icon-printer"
:disabled="!comprehensiveReport"
@click="printReport"
>打印报告</el-button>
2025-11-26 14:23:53 +08:00
<el-button
type="primary"
icon="el-icon-download"
:disabled="!comprehensiveReport"
@click="exportReport('word')"
>导出报告</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
2025-12-18 09:00:51 +08:00
import { getUserAssessmentSummary, getStudentOptions, listAssessment } from '@/api/psychology/assessment'
2025-11-27 18:12:23 +08:00
import { getProfileByUserId, listProfile } from '@/api/psychology/profile'
import { getReport, listReport } from '@/api/psychology/report'
import { parseTime } from '@/utils/common'
2025-11-26 14:23:53 +08:00
import axios from 'axios'
export default {
name: 'ComprehensiveAssessment',
data() {
return {
selectedUserId: undefined,
2025-11-27 18:12:23 +08:00
userSearchKeyword: '',
userSearchLoading: false,
cachedUserOptions: [],
2025-11-26 14:23:53 +08:00
userProfile: null,
userSummary: null,
reportOptions: [],
selectedReports: [],
loading: false,
generating: false,
reportDialogVisible: false,
comprehensiveReport: '',
ragSourcesForReport: [], // RAG知识库来源
2025-12-21 18:57:14 +08:00
// ========== 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'
2025-12-19 09:03:26 +08:00
2025-12-21 18:57:14 +08:00
// ========== 备用配置Kimi API - 本地开发)==========
// API_URL: 'https://api.moonshot.cn/v1/chat/completions',
// API_KEY: 'sk-U9fdriPxwBcrpWW0Ite3N0eVtX7VxnqqqYUIBAdWd1hgEA9m',
// MODEL: 'moonshot-v1-32k'
2025-11-26 14:23:53 +08:00
}
},
created() {
},
methods: {
2025-11-27 18:12:23 +08:00
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([])
2025-11-28 17:31:13 +08:00
// 如果是纯数字,通过信息编号搜索
2025-11-27 18:12:23 +08:00
if (/^\d+$/.test(trimmed)) {
profilePromise = listProfile({
infoNumber: trimmed,
pageNum: 1,
pageSize: 20
})
.then((res) => this.normalizeProfileOptions(res.rows || []))
.catch(() => [])
2025-11-28 17:31:13 +08:00
} else {
// 如果不是纯数字(输入姓名),通过姓名搜索档案以获取编号信息
profilePromise = listProfile({
userName: trimmed,
pageNum: 1,
pageSize: 20
})
.then((res) => this.normalizeProfileOptions(res.rows || []))
.catch(() => [])
2025-11-27 18:12:23 +08:00
}
return Promise.all([studentPromise, profilePromise])
.then(([studentList, profileList]) => {
2025-11-28 17:31:13 +08:00
// 合并时优先使用档案数据因为档案数据包含完整的infoNumber
const merged = this.mergeUserOptions([...profileList, ...studentList])
2025-11-27 18:12:23 +08:00
this.cachedUserOptions = merged
return merged
2025-11-26 14:23:53 +08:00
})
.finally(() => {
2025-11-27 18:12:23 +08:00
this.userSearchLoading = false
2025-11-26 14:23:53 +08:00
})
},
buildUserLabel(option) {
if (!option) {
return ''
}
const name = option.nickName || option.userName || ''
2025-11-27 18:12:23 +08:00
const info = option.infoNumber ? `(编号:${option.infoNumber}` : ''
2025-11-26 14:23:53 +08:00
const dept = option.deptName ? ` - ${option.deptName}` : ''
2025-11-27 18:12:23 +08:00
return `${name}${info}${dept}`
2025-11-26 14:23:53 +08:00
},
2025-11-27 18:12:23 +08:00
handleUserSelect(option) {
if (!option || !option.userId) {
return
2025-11-26 14:23:53 +08:00
}
2025-11-27 18:12:23 +08:00
this.selectedUserId = option.userId
this.userSearchKeyword = this.buildUserLabel(option)
},
handleManualSearch() {
const keyword = (this.userSearchKeyword || '').trim()
if (!keyword) {
this.$message.warning('请输入姓名或信息编号')
return
}
2025-12-02 15:12:55 +08:00
2025-11-28 17:31:13 +08:00
// 从输入框中提取纯姓名或编号(去除括号、编号等格式)
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()
}
}
2025-12-02 15:12:55 +08:00
2025-11-28 17:31:13 +08:00
this.fetchUserOptions(searchKeyword).then((list) => {
2025-11-27 18:12:23 +08:00
if (!list.length) {
2025-11-28 17:31:13 +08:00
// 不显示错误消息,让用户继续搜索或选择
// 如果用户已经选择了用户,保持选择状态
2025-11-27 18:12:23 +08:00
return
}
if (list.length === 1) {
2025-11-28 17:31:13 +08:00
// 找到唯一结果,自动选择
2025-11-27 18:12:23 +08:00
this.handleUserSelect(list[0])
} else {
2025-11-28 17:31:13 +08:00
// 如果找到多条,尝试精确匹配输入框的完整内容
const exactMatch = list.find(opt => {
const label = this.buildUserLabel(opt)
return label === keyword
})
if (exactMatch) {
// 精确匹配成功,自动选择
this.handleUserSelect(exactMatch)
} else {
// 多条记录,提示用户从下拉列表选择
this.$message.info('找到多条记录,请从下拉列表选择具体用户')
}
2025-11-27 18:12:23 +08:00
}
2025-11-28 17:31:13 +08:00
}).catch(() => {
// 搜索失败时,不显示错误消息,让用户继续操作
2025-11-27 18:12:23 +08:00
})
},
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)
2025-11-28 17:31:13 +08:00
} 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而新项没有则保留已存在的
// 不做任何操作
}
2025-11-27 18:12:23 +08:00
}
})
return Array.from(map.values())
2025-11-26 14:23:53 +08:00
},
async loadUserData() {
if (!this.selectedUserId) {
this.$message.warning('请先选择用户')
return
}
this.loading = true
try {
2025-12-18 09:00:51 +08:00
// 直接加载该用户的所有测评记录(不使用汇总接口,避免被过滤)
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'
})
}
}
// 获取用户基本信息(用于显示)
2025-11-26 14:23:53 +08:00
const summaryResponse = await getUserAssessmentSummary(this.selectedUserId)
this.userSummary = summaryResponse.data || null
2025-12-18 09:00:51 +08:00
// 加载问卷报告
2025-11-27 18:12:23 +08:00
const questionnaireReports = await this.loadQuestionnaireReports(this.selectedUserId)
2025-12-18 09:00:51 +08:00
// 合并量表和问卷报告
2025-11-27 18:12:23 +08:00
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
2025-11-26 14:23:53 +08:00
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
2025-11-27 18:12:23 +08:00
this.userSearchKeyword = ''
2025-11-26 14:23:53 +08:00
this.userProfile = null
this.userSummary = null
this.reportOptions = []
this.selectedReports = []
this.comprehensiveReport = ''
},
2025-11-27 18:12:23 +08:00
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 []
}
},
2025-11-26 14:23:53 +08:00
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
}
2025-12-19 09:03:26 +08:00
// 先保存选中的报告副本,防止在异步操作中被修改
const selectedReportsCopy = [...this.selectedReports]
console.log('generateComprehensiveReport - 选中报告数:', selectedReportsCopy.length, selectedReportsCopy)
2025-11-26 14:23:53 +08:00
this.generating = true
this.reportDialogVisible = true
this.comprehensiveReport = ''
try {
2025-12-19 09:03:26 +08:00
// 1. 获取选中量表的报告内容(传入副本)
const scaleReports = await this.fetchSelectedReports(selectedReportsCopy)
2025-11-26 14:23:53 +08:00
// 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
}
},
2025-12-19 09:03:26 +08:00
async fetchSelectedReports(selectedReportsList) {
// 使用传入的列表,而不是 this.selectedReports
const reportsToProcess = selectedReportsList || this.selectedReports || []
console.log('fetchSelectedReports - 待处理报告数:', reportsToProcess.length, reportsToProcess)
2025-11-26 14:23:53 +08:00
const reports = []
2025-12-19 09:03:26 +08:00
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)
2025-11-26 14:23:53 +08:00
}
}
2025-12-19 09:03:26 +08:00
// 无论是否获取到详细内容,都添加到报告列表
reports.push({
scaleName: row.scaleName || row.reportTitle || '未知量表',
submitTime: row.submitTime,
totalScore: row.totalScore,
summary: summary,
content: reportContent,
sourceType
})
2025-11-26 14:23:53 +08:00
}
2025-12-19 09:03:26 +08:00
console.log('fetchSelectedReports - 返回报告数:', reports.length, reports)
2025-11-26 14:23:53 +08:00
return reports
},
buildUserInfoSummary() {
if (!this.userProfile) {
return {
userName: this.userSummary?.userName || this.userSummary?.nickName || '未知',
2025-12-19 09:03:26 +08:00
infoNumber: '-',
gender: '-',
2025-11-26 14:23:53 +08:00
age: '-',
2025-12-19 09:03:26 +08:00
birthday: '-',
nation: '-',
educationLevel: '-',
2025-11-26 14:23:53 +08:00
crimeName: '-',
sentenceTerm: '-',
2025-12-19 09:03:26 +08:00
sentenceStartDate: '-',
2025-11-26 14:23:53 +08:00
sentenceEndDate: '-',
2025-12-19 09:03:26 +08:00
entryDate: '-',
prisonArea: '-',
status: '-'
2025-11-26 14:23:53 +08:00
}
}
2025-12-19 09:03:26 +08:00
// 状态映射
const statusMap = { '0': '在押', '1': '释放', '2': '外出', '3': '假释' }
const genderMap = { '0': '男', '1': '女', '2': '未知' }
2025-11-26 14:23:53 +08:00
return {
userName: this.userProfile.userName || this.userSummary?.userName || '未知',
2025-12-19 09:03:26 +08:00
infoNumber: this.userProfile.infoNumber || '-',
gender: genderMap[this.userProfile.gender] || this.userProfile.gender || '-',
2025-11-26 14:23:53 +08:00
age: this.calculateAge(this.userProfile.birthday),
2025-12-19 09:03:26 +08:00
birthday: this.formatDate(this.userProfile.birthday) || '-',
nation: this.userProfile.nation || '-',
educationLevel: this.userProfile.educationLevel || '-',
2025-11-26 14:23:53 +08:00
crimeName: this.userProfile.crimeName || '-',
sentenceTerm: this.userProfile.sentenceTerm || '-',
2025-12-19 09:03:26 +08:00
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 || '-'
2025-11-26 14:23:53 +08:00
}
},
buildAIPrompt(userInfo, scaleReports) {
2025-12-19 09:03:26 +08:00
// 构建量表清单
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')
2025-11-26 14:23:53 +08:00
const SYSTEM_PROMPT = [
2025-12-19 09:03:26 +08:00
'你是一名资深的监狱心理矫治专家和心理测评分析师,拥有丰富的罪犯心理评估和矫治工作经验。',
'请根据提供的服刑人员个人信息和多个心理量表/问卷测评结果,生成一份专业、详细、深入的综合心理评估报告。',
'',
'【重要提示】本次分析涉及以下量表/问卷,请在报告中对每一个进行深入分析:',
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标签'
2025-11-26 14:23:53 +08:00
].join('\n')
const userInfoText = `
2025-12-19 09:03:26 +08:00
被测试者完整个人信息
2025-11-26 14:23:53 +08:00
- 姓名${userInfo.userName}
2025-12-19 09:03:26 +08:00
- 信息编号${userInfo.infoNumber}
- 性别${userInfo.gender}
2025-11-26 14:23:53 +08:00
- 年龄${userInfo.age}
2025-12-19 09:03:26 +08:00
- 出生日期${userInfo.birthday}
- 民族${userInfo.nation}
- 文化程度${userInfo.educationLevel}
2025-11-26 14:23:53 +08:00
- 罪名${userInfo.crimeName}
- 刑期${userInfo.sentenceTerm}
2025-12-19 09:03:26 +08:00
- 刑期起日${userInfo.sentenceStartDate}
2025-11-26 14:23:53 +08:00
- 刑期止日${userInfo.sentenceEndDate}
2025-12-19 09:03:26 +08:00
- 入监时间${userInfo.entryDate}
- 所在监区${userInfo.prisonArea}
- 当前状态${userInfo.status}
2025-11-26 14:23:53 +08:00
`.trim()
const scaleReportsText = scaleReports
.map((report, index) => {
2025-11-27 18:12:23 +08:00
const typeLabel = report.sourceType === 'questionnaire' ? '问卷' : '量表'
2025-12-19 09:03:26 +08:00
// 提取完整内容以便AI更好地分析
2025-11-26 14:23:53 +08:00
const contentText = report.content
.replace(/<[^>]*>/g, '')
.replace(/&nbsp;/g, ' ')
2025-12-19 09:03:26 +08:00
.replace(/\s+/g, ' ')
.substring(0, 2000) // 增加内容长度以获取更多因子信息
2025-11-26 14:23:53 +08:00
return `
2025-12-19 09:03:26 +08:00
${typeLabel}${index + 1}${report.scaleName}
- 测评时间${this.formatDateTime(report.submitTime)}
- 总分${report.totalScore}
- 报告摘要${report.summary || '无'}
- 详细内容包含各因子得分${contentText}...
2025-11-26 14:23:53 +08:00
`.trim()
})
.join('\n\n')
2025-12-19 09:03:26 +08:00
return `${SYSTEM_PROMPT}\n\n${userInfoText}\n\n【各量表/问卷详细报告及因子分析】\n${scaleReportsText}`
2025-11-26 14:23:53 +08:00
},
2025-12-21 18:57:14 +08:00
// Ollama 本地大模型调用方法
2025-11-26 14:23:53 +08:00
async callOLLAMA(prompt) {
try {
// 1. 先调用RAG服务获取知识库上下文
let knowledgeContext = '';
let ragSources = [];
2025-12-21 18:57:14 +08:00
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请结合以上专业心理学资料进行分析使报告更加专业和有深度。';
}
2025-12-21 18:57:14 +08:00
// 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;
}
2025-11-26 14:23:53 +08:00
// 清理响应内容
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()
2025-12-02 15:12:55 +08:00
console.log('清理后响应:', response)
2025-11-26 14:23:53 +08:00
if (!response) {
2025-12-02 15:12:55 +08:00
throw new Error('AI分析返回结果为空')
2025-11-26 14:23:53 +08:00
}
return response
} catch (error) {
2025-12-02 15:12:55 +08:00
console.error('AI分析失败:', error)
throw new Error('AI分析失败' + (error.message || '未知错误'))
2025-11-26 14:23:53 +08:00
}
},
formatReport(aiReport, userInfo, scaleReports) {
2025-12-19 09:03:26 +08:00
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>'
}
2025-11-26 14:23:53 +08:00
// 构建完整的报告HTML
const reportHtml = `
<div class="comprehensive-report">
2025-12-19 09:03:26 +08:00
<h1 style="text-align: center; margin-bottom: 30px;">综合心理评估报告</h1>
2025-12-02 15:12:55 +08:00
2025-11-26 14:23:53 +08:00
<div class="user-info-section" style="margin-bottom: 30px; padding: 15px; background-color: #f5f7fa; border-radius: 4px;">
2025-12-19 09:03:26 +08:00
<h2 style="margin-top: 0;">被测试者基本信息</h2>
2025-11-26 14:23:53 +08:00
<table style="width: 100%; border-collapse: collapse;">
<tr>
2025-12-19 09:03:26 +08:00
<td style="padding: 8px; border: 1px solid #ddd; width: 120px; background-color: #fafafa;"><strong>姓名</strong></td>
2025-11-26 14:23:53 +08:00
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.userName}</td>
2025-12-19 09:03:26 +08:00
<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>
2025-11-26 14:23:53 +08:00
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.age}</td>
</tr>
<tr>
2025-12-19 09:03:26 +08:00
<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>
2025-11-26 14:23:53 +08:00
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.sentenceTerm}</td>
2025-12-19 09:03:26 +08:00
<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>
2025-11-26 14:23:53 +08:00
</tr>
<tr>
2025-12-19 09:03:26 +08:00
<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>
2025-11-26 14:23:53 +08:00
<td style="padding: 8px; border: 1px solid #ddd;">${userInfo.sentenceEndDate}</td>
2025-12-19 09:03:26 +08:00
</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>
2025-11-26 14:23:53 +08:00
</tr>
</table>
</div>
<div class="scale-summary" style="margin-bottom: 30px;">
2025-12-19 09:03:26 +08:00
<h2>参与测评的量表/问卷共${scaleReports ? scaleReports.length : 0}</h2>
<ul style="line-height: 2; padding-left: 20px;">
${scaleListHtml}
2025-11-26 14:23:53 +08:00
</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;">
2025-11-27 18:12:23 +08:00
<div>报告生成时间${parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}')}</div>
2025-12-02 15:12:55 +08:00
<div style="margin-top: 8px;">被评估人<span style="color: #303133; font-weight: bold; text-decoration: underline;">______________________</span></div>
2025-11-26 14:23:53 +08:00
</div>
2025-12-19 09:03:26 +08:00
<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>
` : ''}
2025-11-26 14:23:53 +08:00
</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
},
2025-11-27 18:12:23 +08:00
buildReportHtml() {
return `
2025-11-26 14:23:53 +08:00
<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>
`
2025-11-27 18:12:23 +08:00
},
exportReport(format) {
if (!this.comprehensiveReport) {
this.$message.warning('报告内容为空')
return
}
const reportHtml = this.buildReportHtml()
2025-11-26 14:23:53 +08:00
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 {
2025-11-27 18:12:23 +08:00
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
2025-11-26 14:23:53 +08:00
}
2025-11-27 18:12:23 +08:00
printWindow.document.write(reportHtml)
printWindow.document.close()
printWindow.focus()
printWindow.print()
2025-11-26 14:23:53 +08:00
}
}
}
</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>