- 重命名文件: ruoyi.js common.js, ruoyi.scss common.scss - 重命名组件: RuoYi/ Common/ - 创建新类: XinliConfig.java (替代RuoYiConfig.java) - 更新所有导入语句和引用 (50+ 处) - 更新配置前缀: ruoyi xinli - 更新Swagger文档标题和描述 - 更新许可证版权信息 - 移除所有RuoYi文档链接和示例代码
1101 lines
43 KiB
Vue
1101 lines
43 KiB
Vue
<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(/ /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>
|
||
|