xinli/xinli-ui/src/views/psychology/report/detail.vue
2025-11-20 20:42:39 +08:00

559 lines
19 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="app-container">
<el-card v-loading="loading">
<div slot="header" class="clearfix">
<span>测评报告详情</span>
<div style="float: right;">
<el-button
style="padding: 3px 0; margin-right: 10px;"
type="text"
icon="el-icon-edit"
@click="handleEdit"
v-hasPermi="['psychology:report:edit']"
>编辑</el-button>
<el-button style="padding: 3px 0" type="text" @click="handleBack">返回</el-button>
</div>
</div>
<el-descriptions title="基本信息" :column="2" border>
<el-descriptions-item label="报告ID">{{ reportForm.reportId }}</el-descriptions-item>
<el-descriptions-item label="来源类型">
<el-tag v-if="sourceType === 'questionnaire'" type="warning">问卷</el-tag>
<el-tag v-else type="primary">量表</el-tag>
</el-descriptions-item>
<el-descriptions-item label="来源ID">{{ reportForm.assessmentId || reportForm.answerId }}</el-descriptions-item>
<el-descriptions-item label="报告标题" :span="2">{{ reportForm.reportTitle || '-' }}</el-descriptions-item>
<el-descriptions-item label="报告类型">
<el-tag v-if="reportForm.reportType === 'standard'" type="">标准报告</el-tag>
<el-tag v-else-if="reportForm.reportType === 'detailed'" type="success">详细报告</el-tag>
<el-tag v-else-if="reportForm.reportType === 'brief'" type="info">简要报告</el-tag>
</el-descriptions-item>
<el-descriptions-item label="生成状态">
<el-tag v-if="reportForm.isGenerated === '0'" type="warning">未生成</el-tag>
<el-tag v-else-if="reportForm.isGenerated === '1'" type="success">已生成</el-tag>
</el-descriptions-item>
<el-descriptions-item label="生成时间">{{ parseTime(reportForm.generateTime, '{y}-{m}-{d} {h}:{i}:{s}') || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ parseTime(reportForm.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
</el-descriptions>
<el-divider content-position="left">报告摘要</el-divider>
<div class="summary-content" v-html="reportForm.summary || '暂无摘要'"></div>
<el-divider content-position="left">报告内容</el-divider>
<div v-if="!reportForm.reportContent" style="padding: 20px; text-align: center; color: #999;">
报告内容正在生成中...
</div>
<div v-else class="report-content" v-html="reportForm.reportContent"></div>
<!-- 问卷成绩排名 -->
<el-divider v-if="sourceType === 'questionnaire' && reportForm.answerId" content-position="left">成绩排名</el-divider>
<div v-if="sourceType === 'questionnaire' && reportForm.answerId" class="rank-section">
<el-button type="primary" size="small" @click="loadRankList" :loading="rankLoading">查看排名</el-button>
<el-table v-if="rankList.length > 0" :data="rankList" border style="width: 100%; margin-top: 15px;">
<el-table-column type="index" label="排名" width="80" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.$index + 1 === 1" type="danger">第1名</el-tag>
<el-tag v-else-if="scope.$index + 1 === 2" type="warning">第2名</el-tag>
<el-tag v-else-if="scope.$index + 1 === 3" type="success">第3名</el-tag>
<span v-else>第{{ scope.$index + 1 }}名</span>
</template>
</el-table-column>
<el-table-column prop="respondentName" label="答题人" width="150" />
<el-table-column prop="totalScore" label="总分" width="120" align="center" />
<el-table-column prop="submitTime" label="提交时间" width="180" align="center">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.submitTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
</el-table>
</div>
<el-divider v-if="reportForm.pdfPath" content-position="left">PDF下载</el-divider>
<div v-if="reportForm.pdfPath">
<el-button type="primary" icon="el-icon-download" @click="handleDownloadPDF">下载PDF报告</el-button>
</div>
<!-- AI分析 -->
<el-divider content-position="left">AI智能分析</el-divider>
<div class="ai-analysis-section">
<el-button
type="primary"
icon="el-icon-magic-stick"
@click="handleAIAnalysis"
:loading="aiLoading"
:disabled="!reportForm.reportContent"
>
{{ aiLoading ? '分析中...' : 'AI分析' }}
</el-button>
<div v-if="aiError" class="ai-error">
<el-alert
:title="aiError"
type="error"
:closable="false"
show-icon>
</el-alert>
</div>
<div v-if="aiResult" class="ai-result">
<h3 class="ai-result-title">
<i class="el-icon-magic-stick"></i> AI分析结果
</h3>
<div class="ai-result-content" v-html="aiResult"></div>
</div>
</div>
</el-card>
<!-- 报告编辑对话框 -->
<el-dialog title="编辑报告" :visible.sync="editOpen" width="900px" append-to-body>
<el-form ref="editForm" :model="editForm" :rules="editRules" label-width="100px">
<el-form-item label="报告标题" prop="reportTitle">
<el-input v-model="editForm.reportTitle" placeholder="请输入报告标题" />
</el-form-item>
<el-form-item label="报告类型" prop="reportType">
<el-select v-model="editForm.reportType" placeholder="请选择报告类型">
<el-option label="标准报告" value="standard" />
<el-option label="详细报告" value="detailed" />
<el-option label="简要报告" value="brief" />
</el-select>
</el-form-item>
<el-form-item label="报告摘要" prop="summary">
<Editor v-model="editForm.summary" :min-height="150" />
</el-form-item>
<el-form-item label="报告内容" prop="reportContent">
<Editor v-model="editForm.reportContent" :min-height="400" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitEditForm">确 定</el-button>
<el-button @click="cancelEdit"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getReport, getReportByAssessmentId, updateReportWithType } from "@/api/psychology/report";
import { getQuestionnaireRankList } from "@/api/psychology/questionnaireAnswer";
import request from '@/utils/request';
import axios from 'axios';
import Editor from "@/components/Editor";
export default {
name: "ReportDetail",
components: { Editor },
data() {
return {
loading: true,
reportForm: {},
sourceType: null,
rankList: [],
rankLoading: false,
// AI分析
aiLoading: false,
aiResult: '',
aiError: '',
// 编辑对话框
editOpen: false,
editForm: {},
editRules: {
reportTitle: [
{ required: true, message: "报告标题不能为空", trigger: "blur" }
],
reportType: [
{ required: true, message: "报告类型不能为空", trigger: "change" }
]
}
};
},
created() {
this.loadReport();
},
methods: {
/** 加载报告 */
loadReport() {
this.loading = true;
const reportId = this.$route.query.reportId;
const assessmentId = this.$route.query.assessmentId;
this.sourceType = this.$route.query.sourceType;
if (!reportId && !assessmentId) {
this.loading = false;
this.$modal.msgError("缺少报告ID或测评ID参数");
this.$router.push('/psychology/report');
return;
}
// 如果有reportId根据sourceType查询
if (reportId) {
console.log('开始加载报告reportId:', reportId, 'sourceType:', this.sourceType);
getReport(reportId, this.sourceType).then(response => {
console.log('报告加载响应:', response);
if (response && response.data) {
console.log('报告数据:', response.data);
console.log('报告内容:', response.data.reportContent);
this.reportForm = response.data;
} else {
console.warn('报告数据为空');
this.$modal.msgWarning("报告不存在");
}
this.loading = false;
}).catch(error => {
this.loading = false;
console.error('加载报告失败:', error);
console.error('错误详情:', error.response || error.message);
this.$modal.msgError("加载报告失败,请检查报告是否存在");
});
} else {
// 使用测评ID查询
getReportByAssessmentId(assessmentId).then(response => {
if (response.data) {
this.reportForm = response.data;
} else {
this.$modal.msgWarning("报告不存在");
}
this.loading = false;
}).catch(error => {
this.loading = false;
console.error('加载报告失败:', error);
this.$modal.msgError("加载报告失败,请检查报告是否存在");
});
}
},
/** 返回 */
handleBack() {
this.$router.back();
},
/** 下载PDF */
handleDownloadPDF() {
if (this.reportForm.pdfPath) {
window.open(this.reportForm.pdfPath);
} else {
this.$modal.msgWarning("PDF文件不存在");
}
},
/** 加载排名列表 */
loadRankList() {
if (!this.reportForm.answerId) {
this.$modal.msgWarning("无法获取问卷ID");
return;
}
// 从答题记录中获取问卷ID
request({
url: '/psychology/questionnaire/answer/' + this.reportForm.answerId,
method: 'get'
}).then(response => {
if (response.data && response.data.questionnaireId) {
this.rankLoading = true;
getQuestionnaireRankList(response.data.questionnaireId).then(rankResponse => {
this.rankList = rankResponse.data || [];
this.rankLoading = false;
}).catch(error => {
this.rankLoading = false;
console.error('加载排名失败:', error);
this.$modal.msgError("加载排名失败");
});
} else {
this.$modal.msgWarning("无法获取问卷ID");
}
}).catch(error => {
console.error('获取答题记录失败:', error);
this.$modal.msgError("获取答题记录失败");
});
},
/** 编辑按钮操作 */
handleEdit() {
this.editForm = {
reportId: this.reportForm.reportId,
sourceType: this.sourceType,
reportTitle: this.reportForm.reportTitle || '',
reportType: this.reportForm.reportType || 'standard',
summary: this.reportForm.summary || '',
reportContent: this.reportForm.reportContent || ''
};
this.editOpen = true;
},
/** 提交编辑表单 */
submitEditForm() {
this.$refs["editForm"].validate(valid => {
if (valid) {
const updateData = {
reportTitle: this.editForm.reportTitle,
reportType: this.editForm.reportType,
summary: this.editForm.summary,
reportContent: this.editForm.reportContent
};
updateReportWithType(this.editForm.reportId, this.editForm.sourceType, updateData).then(response => {
this.$modal.msgSuccess("修改成功");
this.editOpen = false;
// 重新加载报告数据
this.loadReport();
}).catch(error => {
console.error('修改报告失败:', error);
this.$modal.msgError("修改失败");
});
}
});
},
/** 取消编辑 */
cancelEdit() {
this.editOpen = false;
this.resetEditForm();
},
/** 重置编辑表单 */
resetEditForm() {
this.editForm = {
reportId: null,
sourceType: null,
reportTitle: '',
reportType: 'standard',
summary: '',
reportContent: ''
};
if (this.$refs["editForm"]) {
this.$refs["editForm"].resetFields();
}
},
/** AI分析 */
async handleAIAnalysis() {
if (!this.reportForm.reportContent) {
this.$modal.msgWarning("报告内容为空,无法进行分析");
return;
}
this.aiLoading = true;
this.aiError = '';
this.aiResult = '';
// Ollama API配置
const OLLAMA_URL = 'http://192.168.0.106:11434/api/generate';
const MODEL = 'deepseek-r1:32b';
// 构建系统提示词
const SYSTEM_PROMPT = [
'你是专业心理测评报告分析师,请根据用户提供的报告内容进行深度分析。要求:',
'1. 提取报告的核心信息和关键指标;',
'2. 分析测评结果的含义和可能的影响;',
'3. 提供专业、客观、易懂的分析解读500-800字',
'4. 使用结构化的格式输出,包含:核心结论、详细分析、建议、总体结论四个部分;',
'5. 仅输出分析结果,不添加额外建议、问候语或思考过程;',
'6. 使用HTML格式输出使用<h3>标签作为小标题,<p>标签作为段落。'
].join('\n');
// 构建完整的提示词
const reportContent = this.reportForm.reportContent || '';
const reportTitle = this.reportForm.reportTitle || '心理测评报告';
const reportType = this.reportForm.reportType || '标准报告';
// 提取纯文本内容去除HTML标签
const textContent = reportContent.replace(/<[^>]*>/g, '').substring(0, 3000);
const prompt = `${SYSTEM_PROMPT}\n\n重要请直接输出结果不要包含任何思考过程、<think>标签或<think>标签。\n\n报告标题${reportTitle}\n报告类型${reportType}\n报告内容${textContent}`;
try {
const { data } = await axios.post(OLLAMA_URL, {
model: MODEL,
prompt: prompt,
temperature: 0.2,
num_predict: 1000,
stream: false
}, {
timeout: 60000 // 60秒超时
});
let rawResponse = data?.response ?? '无法解析模型输出';
// 过滤掉思考过程标签
rawResponse = rawResponse
.replace(/<think>[\s\S]*?<\/think>/gi, '')
.replace(/<think>[\s\S]*?<\/redacted_reasoning>/gi, '')
.replace(/<think[\s\S]*?>/gi, '')
.replace(/<redacted_reasoning[\s\S]*?>/gi, '')
// 移除Markdown代码块标记
.replace(/```html\s*/gi, '')
.replace(/```\s*/g, '')
.replace(/```[a-z]*\s*/gi, '')
.trim();
if (!rawResponse || rawResponse === '无法解析模型输出') {
this.aiError = '模型返回结果为空,请稍后重试';
return;
}
// 格式化结果确保HTML格式正确
this.aiResult = this.formatAIResult(rawResponse);
} catch (err) {
console.error('AI分析失败:', err);
if (err.code === 'ECONNABORTED' || err.message.includes('timeout')) {
this.aiError = '请求超时请检查Ollama服务是否正常运行';
} else if (err.response) {
this.aiError = err.response.data?.error || err.message || 'AI分析失败请稍后重试';
} else if (err.request) {
this.aiError = '无法连接到Ollama服务请检查服务地址是否正确当前' + OLLAMA_URL + '';
} else {
this.aiError = err.message || 'AI分析失败请稍后重试';
}
} finally {
this.aiLoading = false;
}
},
/** 格式化AI分析结果 */
formatAIResult(text) {
// 移除Markdown代码块标记```html、```等)
let html = text
.replace(/```html\s*/gi, '')
.replace(/```\s*/g, '')
.replace(/```[a-z]*\s*/gi, '')
.trim();
// 如果已经是HTML格式清理后返回
if (html.includes('<h3>') || html.includes('<p>') || html.includes('<div>')) {
// 移除可能残留的代码块标记
html = html.replace(/```html\s*/gi, '').replace(/```\s*/g, '');
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('');
// 确保每个段落都有正确的格式
html = html.replace(/(<p>.*?<\/p>)/g, (match) => {
if (match.includes('<h3>')) return match.replace(/<p>|<\/p>/g, '');
return match;
});
// 最后再次清理可能残留的代码块标记
html = html.replace(/```html\s*/gi, '').replace(/```\s*/g, '');
return html;
}
}
};
</script>
<style scoped>
.app-container {
padding: 20px;
}
.clearfix:before,
.clearfix:after {
display: table;
content: "";
}
.clearfix:after {
clear: both;
}
.summary-content,
.report-content {
padding: 15px;
background-color: #f8f9fa;
border-radius: 4px;
min-height: 100px;
line-height: 1.8;
color: #333;
}
.summary-content >>> p,
.report-content >>> p {
margin: 10px 0;
}
.summary-content >>> h1,
.report-content >>> h1 {
font-size: 24px;
font-weight: bold;
margin: 15px 0;
}
.summary-content >>> h2,
.report-content >>> h2 {
font-size: 20px;
font-weight: bold;
margin: 12px 0;
}
.summary-content >>> h3,
.report-content >>> h3 {
font-size: 16px;
font-weight: bold;
margin: 10px 0;
}
/* AI分析区域样式 */
.ai-analysis-section {
margin-top: 20px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}
.ai-error {
margin-top: 15px;
}
.ai-result {
margin-top: 20px;
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.ai-result-title {
font-size: 18px;
font-weight: bold;
color: #409eff;
margin: 0 0 15px 0;
padding-bottom: 10px;
border-bottom: 2px solid #e4e7ed;
display: flex;
align-items: center;
gap: 8px;
}
.ai-result-title i {
font-size: 20px;
}
.ai-result-content {
line-height: 1.8;
color: #333;
}
.ai-result-content >>> h3 {
font-size: 16px;
font-weight: bold;
color: #409eff;
margin: 20px 0 10px 0;
padding-left: 10px;
border-left: 3px solid #409eff;
}
.ai-result-content >>> p {
margin: 12px 0;
text-align: justify;
color: #606266;
}
.ai-result-content >>> p:first-of-type {
margin-top: 0;
}
.ai-result-content >>> br {
line-height: 1.8;
}
</style>