xinli/xinli-ui/src/views/psychology/report/index.vue
2025-12-22 15:06:30 +08:00

1362 lines
52 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="100px">
<el-form-item label="用户" prop="userId">
<el-select
v-model="queryParams.userId"
placeholder="请选择用户"
clearable
filterable
style="width: 200px;"
>
<el-option
v-for="user in userList"
:key="user.userId"
:label="user.nickName + ' (' + user.userName + ')'"
:value="user.userId"
/>
</el-select>
</el-form-item>
<el-form-item label="来源类型" prop="sourceType">
<el-select v-model="queryParams.sourceType" placeholder="请选择来源类型" clearable>
<el-option label="量表" value="assessment" />
<el-option label="问卷" value="questionnaire" />
</el-select>
</el-form-item>
<el-form-item label="报告类型" prop="reportType">
<el-select v-model="queryParams.reportType" placeholder="报告类型" clearable>
<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="isGenerated">
<el-select v-model="queryParams.isGenerated" placeholder="全部" clearable>
<el-option label="待评分" value="0" />
<el-option label="已完成" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="el-icon-download"
size="mini"
:disabled="multiple"
@click="handleExport"
v-hasPermi="['psychology:report:export']"
>导出</el-button>
</el-col>
<el-col :span="1.8">
<el-button
type="primary"
plain
icon="el-icon-printer"
size="mini"
:disabled="multiple"
@click="openExportDialog"
v-hasPermi="['psychology:report:export']"
>导出报告</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['psychology:report:remove']"
>删除</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="reportList" @selection-change="handleSelectionChange" @current-change="handleCurrentChange" highlight-current-row>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" prop="reportId" width="80" />
<el-table-column label="来源类型" align="center" prop="sourceType" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.sourceType === 'questionnaire'" type="warning">问卷</el-tag>
<el-tag v-else type="primary">量表</el-tag>
</template>
</el-table-column>
<el-table-column label="信息编号" align="center" prop="infoNumber" width="120">
<template slot-scope="scope">
<span>{{ scope.row.infoNumber || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="报告标题" align="center" prop="reportTitle" :show-overflow-tooltip="true" />
<el-table-column label="报告类型" align="center" prop="reportType" width="120">
<template slot-scope="scope">
<el-tag v-if="scope.row.reportType === 'standard'" type="">标准报告</el-tag>
<el-tag v-else-if="scope.row.reportType === 'detailed'" type="success">详细报告</el-tag>
<el-tag v-else-if="scope.row.reportType === 'brief'" type="info">简要报告</el-tag>
</template>
</el-table-column>
<el-table-column label="生成状态" align="center" prop="isGenerated" width="100">
<template slot-scope="scope">
<el-tag v-if="scope.row.isGenerated === '0'" type="warning">待评分</el-tag>
<el-tag v-else-if="scope.row.isGenerated === '1'" type="success">已完成</el-tag>
</template>
</el-table-column>
<el-table-column label="生成时间" align="center" prop="generateTime" width="180">
<template slot-scope="scope">
<span v-if="scope.row.generateTime">{{ parseTime(scope.row.generateTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleView(scope.row)"
v-hasPermi="['psychology:report:query']"
>查看</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['psychology:report:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['psychology:report:remove']"
>删除</el-button>
<el-button
v-if="scope.row.sourceType === 'questionnaire'"
size="mini"
type="text"
icon="el-icon-refresh"
@click="handleRegenerate(scope.row)"
v-hasPermi="['psychology:report:edit']"
>重新生成</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 报告编辑对话框 -->
<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>
<!-- SAS 报告导出设置 -->
<el-dialog title="报告导出" :visible.sync="sasExportDialog" width="480px" append-to-body @close="resetSasDialog">
<el-form :model="sasExportForm" label-width="110px">
<el-form-item label="导出格式">
<el-radio-group v-model="sasExportForm.format">
<el-radio label="word">Word 文档</el-radio>
<el-radio label="print">打印/PDF</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="包含AI分析">
<el-switch v-model="sasExportForm.includeAI"></el-switch>
<div style="font-size: 12px; color: #909399; margin-top: 5px;">
开启后将自动生成AI分析并包含在报告中需要约30-60秒
</div>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="sasExportDialog = false">取 消</el-button>
<el-button type="primary" :loading="sasExportLoading" @click="confirmSasExport">
{{ sasExportLoading ? '处理中...' : '开始导出' }}
</el-button>
</div>
</el-dialog>
<!-- 批量导出报告对话框 -->
<el-dialog title="批量导出报告" :visible.sync="batchExportDialog" width="800px" append-to-body @close="resetBatchExportDialog">
<el-form :model="batchExportForm" label-width="110px">
<el-form-item label="导出格式">
<el-radio-group v-model="batchExportForm.format">
<el-radio label="pdf">PDF 文档 (.pdf)</el-radio>
<el-radio label="docx">Word 文档 (.doc)</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="包含AI分析">
<el-switch v-model="batchExportForm.includeAI"></el-switch>
<div style="font-size: 12px; color: #909399; margin-top: 5px;">
开启后未生成AI分析的报告将自动生成每个报告约需30-60秒
</div>
</el-form-item>
</el-form>
<el-divider content-position="left">选中的报告</el-divider>
<el-table :data="batchExportReports" border style="width: 100%" max-height="300">
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="报告标题" prop="reportTitle" :show-overflow-tooltip="true" />
<el-table-column label="来源类型" prop="sourceType" width="100" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.sourceType === 'questionnaire'" type="warning" size="small">问卷</el-tag>
<el-tag v-else type="primary" size="small">量表</el-tag>
</template>
</el-table-column>
<el-table-column label="AI分析状态" width="180" align="center">
<template slot-scope="scope">
<span v-if="scope.row.aiAnalysisTime" style="color: #67C23A;">
<i class="el-icon-check"></i> {{ parseTime(scope.row.aiAnalysisTime, '{y}-{m}-{d} {h}:{i}') }}
</span>
<span v-else style="color: #909399;">
<i class="el-icon-warning-outline"></i> 未生成
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="100" align="center">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
:loading="scope.row.generating"
:disabled="scope.row.generating"
@click="generateSingleAI(scope.row, scope.$index)"
>
{{ scope.row.generating ? '生成中...' : '生成AI' }}
</el-button>
</template>
</el-table-column>
</el-table>
<div slot="footer" class="dialog-footer">
<el-button @click="batchExportDialog = false">取 消</el-button>
<el-button type="primary" :loading="batchExportLoading" @click="confirmBatchExport">
{{ batchExportLoading ? '导出中...' : '开始导出' }}
</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listReport, getReport, delReport, exportReport, updateReportWithType } from "@/api/psychology/report";
import { loadSasReportData } from "@/services/report/ReportDataMapper";
import SASReportGenerator from "@/services/report/SASReportGenerator";
import Editor from "@/components/Editor";
import { listUser } from "@/api/system/user";
import { regenerateQuestionnaireReport } from "@/api/psychology/questionnaireAnswer";
import axios from 'axios';
export default {
name: "Report",
components: { Editor },
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 选中的完整行数据包含sourceType
selectedRows: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 报告表格数据
reportList: [],
currentRow: null,
// 用户列表
userList: [],
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
userId: undefined,
sourceType: undefined,
reportType: undefined,
isGenerated: undefined
},
// 编辑对话框
editOpen: false,
editForm: {},
editRules: {
reportTitle: [
{ required: true, message: "报告标题不能为空", trigger: "blur" }
],
reportType: [
{ required: true, message: "报告类型不能为空", trigger: "change" }
]
},
sasExportDialog: false,
sasExportForm: {
format: "word"
},
sasExportLoading: false,
sasTarget: null,
// 批量导出相关
batchExportDialog: false,
batchExportForm: {
format: "pdf",
includeAI: false
},
batchExportReports: [],
batchExportLoading: false
};
},
computed: {
},
created() {
this.getList();
this.loadUsers();
},
methods: {
/** 加载用户列表 */
loadUsers() {
listUser({ pageNum: 1, pageSize: 1000 }).then(response => {
this.userList = response.rows || [];
}).catch(error => {
console.error('加载用户列表失败:', error);
});
},
/** 查询报告列表 */
getList() {
this.loading = true;
listReport(this.queryParams).then(response => {
this.reportList = response.rows;
this.total = response.total;
this.loading = false;
});
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.reportId);
this.selectedRows = selection; // 保存选中的完整行数据包含sourceType
this.single = selection.length != 1;
this.multiple = !selection.length;
if (selection.length === 0 && this.currentRow) {
this.single = false;
}
},
handleCurrentChange(current) {
this.currentRow = current;
},
/** 查看按钮操作 */
handleView(row) {
this.$router.push({ path: 'report/detail', query: { reportId: row.reportId, sourceType: row.sourceType } });
},
/** 修改按钮操作 */
handleUpdate(row) {
// 先获取完整的报告数据
getReport(row.reportId, row.sourceType).then(response => {
if (response.data) {
this.editForm = {
reportId: response.data.reportId,
sourceType: row.sourceType,
reportTitle: response.data.reportTitle || '',
reportType: response.data.reportType || 'standard',
summary: response.data.summary || '',
reportContent: response.data.reportContent || ''
};
this.editOpen = true;
} else {
this.$modal.msgError("获取报告数据失败");
}
}).catch(error => {
console.error('获取报告数据失败:', error);
this.$modal.msgError("获取报告数据失败");
});
},
/** 提交编辑表单 */
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.getList();
}).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();
}
},
/** 导出按钮操作 */
handleExport() {
// 如果没有选中任何报告,导出所有符合条件的报告
const reportIds = this.ids.length > 0 ? this.ids : null;
this.$modal.loading("正在导出报告数据...");
exportReport(reportIds, this.queryParams).then(data => {
// 检查返回的是否是blob数据
if (data instanceof Blob) {
// 直接使用blob数据
const blob = data
// 生成文件名
let filename = '报告导出_' + new Date().getTime() + '.xlsx'
if (reportIds && reportIds.length === 1) {
// 如果是单个报告,尝试使用报告标题
const selectedReport = this.reportList.find(report => report.reportId === reportIds[0])
if (selectedReport && selectedReport.reportTitle) {
filename = selectedReport.reportTitle.replace(/[^\w\s-]/g, '') + '_' + new Date().getTime() + '.xlsx'
}
} else if (reportIds && reportIds.length > 1) {
filename = '报告批量导出_' + new Date().getTime() + '.xlsx'
}
// 创建下载链接
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
this.$modal.closeLoading()
this.$modal.msgSuccess("导出成功")
} else {
// 如果不是blob可能是错误信息
this.$modal.closeLoading()
this.$modal.msgError("导出失败:返回数据格式错误")
}
}).catch(error => {
this.$modal.closeLoading()
console.error('导出失败:', error)
const errorMsg = error.message || error.msg || "未知错误"
this.$modal.msgError("导出失败:" + errorMsg)
});
},
/** 打开SAS导出对话框 */
getActiveRow() {
if (this.selectedRows.length === 1) {
return this.selectedRows[0];
}
if (this.currentRow) {
return this.currentRow;
}
return null;
},
async openExportDialog() {
// 如果选中了多条报告,打开批量导出对话框
if (this.selectedRows.length > 1) {
this.openBatchExportDialog();
return;
}
// 获取选中的报告(从选中行或当前行)
const target = this.getActiveRow();
if (!target) {
this.$message.warning("请先选择一条报告");
return;
}
console.log('选中的报告对象:', target);
console.log('报告类型:', target.sourceType);
// 确保有sourceType信息
if (!target.sourceType) {
this.$modal.msgError("报告缺少类型信息,请刷新页面后重试");
return;
}
// 如果是问卷报告,打开问卷导出对话框
if (target.sourceType === 'questionnaire') {
this.sasTarget = { ...target, sourceType: 'questionnaire' };
this.sasExportForm = { format: "word" };
this.sasExportDialog = true;
return;
}
// 量表报告使用SAS模板
try {
const resolved = await this.ensureAssessmentInfo(target);
this.sasTarget = { ...resolved, sourceType: 'assessment' };
this.sasExportForm = { format: "word" };
this.sasExportDialog = true;
} catch (error) {
this.$modal.msgError(error.message || "无法定位测评ID");
}
},
async ensureAssessmentInfo(row) {
if (!row) {
throw new Error("未选择报告");
}
const assessmentId = row.sourceId || row.assessmentId;
if (assessmentId) {
return { ...row, sourceId: assessmentId };
}
const fallbackIds = Array.isArray(this.ids) && this.ids.length === 1 ? this.ids[0] : null;
const reportId = row.reportId || row.id || fallbackIds;
if (!reportId) {
throw new Error("无法定位报告ID");
}
const response = await getReport(reportId, row.sourceType);
if (response && response.data && response.data.assessmentId) {
return { ...row, reportId, sourceId: response.data.assessmentId };
}
throw new Error("无法定位测评ID");
},
resetSasDialog() {
this.sasExportDialog = false;
this.sasExportLoading = false;
this.sasTarget = null;
this.sasExportForm = { format: "word", includeAI: false };
},
async confirmSasExport() {
if (!this.sasTarget) {
this.$message.warning("未选择报告");
return;
}
this.sasExportLoading = true;
try {
// 判断是问卷还是量表
if (this.sasTarget.sourceType === 'questionnaire') {
// 问卷报告导出
await this.exportQuestionnaireReport();
} else {
// 量表报告导出
await this.exportAssessmentReport();
}
this.$modal.msgSuccess("报告已生成");
this.resetSasDialog();
} catch (error) {
console.error("生成报告失败:", error);
this.$modal.msgError("生成失败:" + (error.message || "未知错误"));
} finally {
this.sasExportLoading = false;
}
},
async exportAssessmentReport() {
// 获取测评报告数据后端已生成完整的HTML
const response = await getReport(this.sasTarget.reportId, 'assessment');
if (!response || !response.data) {
throw new Error("获取报告内容失败");
}
const report = response.data;
console.log('测评报告数据:', report);
// 如果需要包含AI分析先生成AI分析
let aiAnalysisHtml = '';
if (this.sasExportForm.includeAI) {
try {
this.$message.info('正在生成AI分析请稍候...');
const aiResult = await this.generateAIAnalysis(
report.reportContent || '',
report.reportTitle || '测评报告',
report.reportType || '标准报告'
);
aiAnalysisHtml = `
<div class="ai-analysis" style="margin-top: 30px; padding: 20px; background-color: #f9f9f9; border-radius: 8px; border-left: 4px solid #67C23A;">
<h2 style="color: #67C23A; margin-top: 0;">
<i style="margin-right: 8px;">🤖</i>AI智能分析
</h2>
<div class="ai-content">
${aiResult}
</div>
</div>
`;
} catch (error) {
console.error('AI分析失败:', error);
this.$message.warning('AI分析失败将继续导出报告不含AI分析');
}
}
// 免责声明
const disclaimerHtml = `
<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>
`;
// 直接使用后端生成的报告内容
const reportHtml = `
<html>
<head>
<meta charset="UTF-8" />
<title>${report.reportTitle || '测评报告'}</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; }
.section { margin-top: 24px; }
.report-info { margin: 5px 0; color: #606266; }
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
table td, table th { border: 1px solid #ddd; padding: 10px; font-size: 14px; }
table th { background-color: #f5f7fa; font-weight: bold; }
.content { margin-top: 24px; line-height: 1.8; }
.summary { background-color: #f0f9ff; padding: 15px; border-left: 4px solid #409EFF; margin: 20px 0; }
.ai-analysis { margin-top: 30px; padding: 20px; background-color: #f9f9f9; border-radius: 8px; }
.ai-content p { line-height: 1.8; margin: 10px 0; }
</style>
</head>
<body>
<h1>${report.reportTitle || '测评报告'}</h1>
${report.summary ? `<div class="summary"><h3>报告摘要</h3><div>${report.summary}</div></div>` : ''}
<div class="content">
${report.reportContent || '暂无报告内容'}
</div>
${aiAnalysisHtml}
${disclaimerHtml}
</body>
</html>
`;
if (this.sasExportForm.format === 'word') {
// 导出为Word
const blob = new Blob(['\ufeff', reportHtml], { type: 'application/msword' });
const filename = `${report.reportTitle || '测评报告'}_${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);
} else {
// 打印
const printWindow = window.open('', '_blank');
if (!printWindow) {
this.$modal.msgError("无法打开打印窗口,请检查浏览器是否阻止了弹窗");
return;
}
printWindow.document.write(reportHtml);
printWindow.document.close();
printWindow.focus();
printWindow.print();
}
},
async exportQuestionnaireReport() {
// 获取问卷报告数据后端已生成完整的HTML包含评语
const response = await getReport(this.sasTarget.reportId, 'questionnaire');
if (!response || !response.data) {
throw new Error("获取报告内容失败");
}
const report = response.data;
console.log('问卷报告数据:', report);
// 如果需要包含AI分析先生成AI分析
let aiAnalysisHtml = '';
if (this.sasExportForm.includeAI) {
try {
this.$message.info('正在生成AI分析请稍候...');
const aiResult = await this.generateAIAnalysis(
report.reportContent || '',
report.reportTitle || '问卷报告',
report.reportType || '标准报告'
);
aiAnalysisHtml = `
<div class="ai-analysis" style="margin-top: 30px; padding: 20px; background-color: #f9f9f9; border-radius: 8px; border-left: 4px solid #67C23A;">
<h2 style="color: #67C23A; margin-top: 0;">
<i style="margin-right: 8px;">🤖</i>AI智能分析
</h2>
<div class="ai-content">
${aiResult}
</div>
</div>
`;
} catch (error) {
console.error('AI分析失败:', error);
this.$message.warning('AI分析失败将继续导出报告不含AI分析');
}
}
// 免责声明
const disclaimerHtml = `
<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>
`;
// 直接使用后端生成的报告内容(已包含评语)
const reportHtml = `
<html>
<head>
<meta charset="UTF-8" />
<title>${report.reportTitle || '问卷报告'}</title>
<style>
body { font-family: 'Microsoft Yahei', sans-serif; padding: 32px; color: #303133; }
h1, h2, h3 { text-align: center; margin: 16px 0; }
.section { margin-top: 24px; }
.report-info { margin: 5px 0; color: #606266; }
table.score-table { width: 100%; border-collapse: collapse; margin: 20px 0; }
table.score-table td, table.score-table th { border: 1px solid #ddd; padding: 10px; font-size: 14px; }
table.score-table th { background-color: #f5f7fa; font-weight: bold; }
.content { margin-top: 24px; line-height: 1.8; }
.ai-analysis { margin-top: 30px; padding: 20px; background-color: #f9f9f9; border-radius: 8px; }
.ai-content p { line-height: 1.8; margin: 10px 0; }
</style>
</head>
<body>
${report.reportContent || '暂无报告内容'}
${aiAnalysisHtml}
${disclaimerHtml}
</body>
</html>
`;
if (this.sasExportForm.format === 'word') {
// 导出为Word
const blob = new Blob(['\ufeff', reportHtml], { type: 'application/msword' });
const filename = `${report.reportTitle || '问卷报告'}_${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);
} else {
// 打印
const printWindow = window.open('', '_blank');
if (!printWindow) {
this.$modal.msgError("无法打开打印窗口,请检查浏览器是否阻止了弹窗");
return;
}
printWindow.document.write(reportHtml);
printWindow.document.close();
printWindow.focus();
printWindow.print();
}
},
/** AI分析报告内容 */
async generateAIAnalysis(reportContent, reportTitle, reportType) {
// ========== Ollama本地大模型配置服务器部署==========
const API_URL = window.location.protocol === 'https:'
? '/ollama/api/chat'
: `http://${window.location.hostname}:11434/api/chat`;
const API_KEY = ''; // 本地模型不需要API Key
const MODEL = 'deepseek-r1:32b';
// ========== 备用配置Kimi API - 本地开发)==========
// const API_URL = 'https://api.moonshot.cn/v1/chat/completions';
// const API_KEY = 'sk-U9fdriPxwBcrpWW0Ite3N0eVtX7VxnqqqYUIBAdWd1hgEA9m';
// const MODEL = 'moonshot-v1-32k';
// 构建系统提示词
const SYSTEM_PROMPT = [
'你是专业心理测评报告分析师,请根据用户提供的报告内容进行深度分析。要求:',
'1. 提取报告的核心信息和关键指标;',
'2. 分析测评结果的含义和可能的影响;',
'3. 提供专业、客观、易懂的分析解读500-800字',
'4. 使用结构化的格式输出,包含:核心结论、详细分析、建议、总体结论四个部分;',
'5. 仅输出分析结果,不添加额外建议、问候语或思考过程;',
'6. 使用HTML格式输出使用<h3>标签作为小标题,<p>标签作为段落。'
].join('\n');
// 提取纯文本内容去除HTML标签
const textContent = reportContent.replace(/<[^>]*>/g, '').substring(0, 3000);
const userPrompt = `重要:请直接输出结果,不要包含任何思考过程、<think>标签或</think>标签。\n\n报告标题${reportTitle}\n报告类型${reportType}\n报告内容${textContent}`;
try {
let rawResponse = '';
if (API_URL.includes('11434')) {
// Ollama 本地模型格式
const { data } = await axios.post(API_URL, {
model: MODEL,
messages: [
{ role: 'system', content: SYSTEM_PROMPT },
{ role: 'user', content: userPrompt }
],
stream: false,
options: {
temperature: 0.2,
num_predict: 2000
}
}, {
headers: {
'Content-Type': 'application/json'
},
timeout: 300000 // Ollama本地模型需要更长时间设置5分钟
});
rawResponse = data?.message?.content ?? '';
console.log('Ollama响应:', rawResponse);
} else {
// OpenAI兼容格式Kimi等
const { data } = await axios.post(API_URL, {
model: MODEL,
messages: [
{ role: 'system', content: SYSTEM_PROMPT },
{ role: 'user', content: userPrompt }
],
temperature: 0.2,
max_tokens: 1000,
stream: false
}, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
timeout: 60000
});
rawResponse = data?.choices?.[0]?.message?.content ?? '';
console.log('OpenAI格式响应:', rawResponse);
}
// 过滤掉思考过程标签
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) {
throw new Error('AI分析返回结果为空');
}
// 格式化结果
return this.formatAIResult(rawResponse);
} catch (err) {
console.error('AI分析失败:', err);
throw new Error('AI分析失败' + (err.message || '未知错误'));
}
},
/** 格式化AI分析结果 */
formatAIResult(text) {
// 移除Markdown代码块标记
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>')) {
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;
},
/** 重新生成报告 */
handleRegenerate(row) {
if (!row.sourceId) {
this.$modal.msgError("无法获取答题记录ID");
return;
}
this.$modal.confirm('是否确认重新生成该问卷报告?').then(() => {
this.loading = true;
return regenerateQuestionnaireReport(row.sourceId);
}).then(() => {
this.$modal.msgSuccess("报告生成成功");
this.getList();
}).catch((error) => {
console.error('生成报告失败:', error);
const errorMsg = error.msg || error.message || "生成报告失败";
this.$modal.msgError(errorMsg);
}).finally(() => {
this.loading = false;
});
},
/** 删除按钮操作 */
handleDelete(row) {
let reportIds;
let sourceType;
if (row && row.reportId) {
// 单个删除
reportIds = [row.reportId];
sourceType = row.sourceType;
} else {
// 批量删除
reportIds = this.ids;
// 检查选中的报告是否都是同一类型
if (this.selectedRows && this.selectedRows.length > 0) {
const types = [...new Set(this.selectedRows.map(r => r.sourceType))];
if (types.length === 1) {
sourceType = types[0];
} else {
// 混合类型,需要分别删除
this.$modal.confirm('选中的报告包含不同类型,将分别删除。是否确认删除?').then(() => {
this.deleteReportsByType();
}).catch(() => {});
return;
}
}
}
this.$modal.confirm('是否确认删除选中的报告?').then(() => {
return delReport(reportIds, sourceType);
}).then(() => {
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch((error) => {
console.error('删除失败:', error);
const errorMsg = error.msg || error.message || "删除失败";
this.$modal.msgError(errorMsg);
});
},
/** 按类型分别删除报告 */
deleteReportsByType() {
// 按sourceType分组
const grouped = {};
this.selectedRows.forEach(row => {
const type = row.sourceType || 'assessment';
if (!grouped[type]) {
grouped[type] = [];
}
grouped[type].push(row.reportId);
});
// 分别删除每组
const deletePromises = Object.keys(grouped).map(type => {
return delReport(grouped[type], type);
});
Promise.all(deletePromises).then(() => {
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch((error) => {
console.error('删除失败:', error);
const errorMsg = error.msg || error.message || "删除失败";
this.$modal.msgError(errorMsg);
});
},
/** 打开批量导出对话框 */
async openBatchExportDialog() {
if (this.selectedRows.length === 0) {
this.$message.warning("请先选择要导出的报告");
return;
}
// 获取每个报告的AI分析状态
this.batchExportReports = [];
this.$modal.loading("正在加载报告信息...");
try {
for (const row of this.selectedRows) {
const response = await getReport(row.reportId, row.sourceType);
const report = response.data || {};
this.batchExportReports.push({
reportId: row.reportId,
sourceType: row.sourceType,
sourceId: row.sourceId,
infoNumber: row.infoNumber || report.infoNumber,
reportTitle: report.reportTitle,
reportType: report.reportType,
summary: report.summary,
reportContent: report.reportContent,
aiAnalysis: report.aiAnalysis || null,
aiAnalysisTime: report.aiAnalysisTime || null,
generating: false
});
}
this.batchExportForm = {
format: "pdf",
includeAI: false
};
this.batchExportDialog = true;
} catch (error) {
console.error('加载报告信息失败:', error);
this.$modal.msgError("加载报告信息失败");
} finally {
this.$modal.closeLoading();
}
},
/** 重置批量导出对话框 */
resetBatchExportDialog() {
this.batchExportDialog = false;
this.batchExportLoading = false;
this.batchExportReports = [];
this.batchExportForm = {
format: "pdf",
includeAI: false
};
},
/** 为单个报告生成AI分析 */
async generateSingleAI(row, index) {
if (row.generating) return;
this.$set(this.batchExportReports[index], 'generating', true);
try {
const aiResult = await this.generateAIAnalysis(
row.reportContent || '',
row.reportTitle || '报告',
'standard'
);
// 保存AI分析结果到数据库
const { saveAiAnalysis } = await import("@/api/psychology/report");
await saveAiAnalysis(row.reportId, aiResult, row.sourceType);
// 更新本地数据
this.$set(this.batchExportReports[index], 'aiAnalysis', aiResult);
this.$set(this.batchExportReports[index], 'aiAnalysisTime', new Date());
this.$message.success("AI分析生成成功");
} catch (error) {
console.error('生成AI分析失败:', error);
this.$message.error("生成AI分析失败" + (error.message || "未知错误"));
} finally {
this.$set(this.batchExportReports[index], 'generating', false);
}
},
/** 确认批量导出 */
async confirmBatchExport() {
if (this.batchExportReports.length === 0) {
this.$message.warning("没有可导出的报告");
return;
}
this.batchExportLoading = true;
try {
// 如果需要包含AI分析先为未生成的报告生成AI分析
if (this.batchExportForm.includeAI) {
const reportsNeedAI = this.batchExportReports.filter(r => !r.aiAnalysis);
if (reportsNeedAI.length > 0) {
this.$message.info(`正在为 ${reportsNeedAI.length} 个报告生成AI分析请稍候...`);
for (let i = 0; i < this.batchExportReports.length; i++) {
const report = this.batchExportReports[i];
if (!report.aiAnalysis) {
try {
this.$set(this.batchExportReports[i], 'generating', true);
const aiResult = await this.generateAIAnalysis(
report.reportContent || '',
report.reportTitle || '报告',
'standard'
);
// 保存AI分析结果
const { saveAiAnalysis } = await import("@/api/psychology/report");
await saveAiAnalysis(report.reportId, aiResult, report.sourceType);
this.$set(this.batchExportReports[i], 'aiAnalysis', aiResult);
this.$set(this.batchExportReports[i], 'aiAnalysisTime', new Date());
} catch (error) {
console.error(`报告 ${report.reportTitle} AI分析失败:`, error);
} finally {
this.$set(this.batchExportReports[i], 'generating', false);
}
}
}
}
}
// 使用JSZip打包导出
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
for (const report of this.batchExportReports) {
// 生成报告HTML内容
let aiAnalysisHtml = '';
if (this.batchExportForm.includeAI && report.aiAnalysis) {
aiAnalysisHtml = `
<div class="ai-analysis" style="margin-top: 30px; padding: 20px; background-color: #f9f9f9; border-radius: 8px; border-left: 4px solid #67C23A;">
<h2 style="color: #67C23A; margin-top: 0;">
<i style="margin-right: 8px;">🤖</i>AI智能分析
</h2>
<div class="ai-content">
${report.aiAnalysis}
</div>
</div>
`;
}
// 免责声明
const disclaimerHtml = `
<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>
`;
const reportHtml = `
<html>
<head>
<meta charset="UTF-8" />
<title>${report.reportTitle || '报告'}</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; }
.section { margin-top: 24px; }
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
table td, table th { border: 1px solid #ddd; padding: 10px; font-size: 14px; }
table th { background-color: #f5f7fa; font-weight: bold; }
.content { margin-top: 24px; line-height: 1.8; }
.ai-analysis { margin-top: 30px; padding: 20px; background-color: #f9f9f9; border-radius: 8px; }
.ai-content p { line-height: 1.8; margin: 10px 0; }
</style>
</head>
<body>
<h1>${report.reportTitle || '报告'}</h1>
<div class="content">
${report.reportContent || '暂无报告内容'}
</div>
${aiAnalysisHtml}
${disclaimerHtml}
</body>
</html>
`;
const scaleName = this.extractScaleName(report.reportTitle || '报告');
const infoNumber = this.sanitizeFilenamePart(report.infoNumber || '-');
const safeFileName = `${scaleName}_${infoNumber}_${report.reportId}`
.replace(/[\\/:*?"<>|]/g, '_')
.substring(0, 80);
if (this.batchExportForm.format === 'docx') {
// Word格式 - 使用完整的Word HTML格式
const wordHtml = `
<html xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:w="urn:schemas-microsoft-com:office:word"
xmlns="http://www.w3.org/TR/REC-html40">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<!--[if gte mso 9]>
<xml>
<w:WordDocument>
<w:View>Print</w:View>
<w:Zoom>100</w:Zoom>
</w:WordDocument>
</xml>
<![endif]-->
<style>
body { font-family: '宋体', 'Microsoft Yahei', sans-serif; font-size: 12pt; }
h1 { font-size: 18pt; text-align: center; }
h2 { font-size: 14pt; }
h3 { font-size: 12pt; }
table { border-collapse: collapse; width: 100%; }
td, th { border: 1px solid #000; padding: 5pt; }
.ai-analysis { background-color: #f0f0f0; padding: 10pt; margin-top: 20pt; }
.disclaimer { background-color: #FDF6EC; border: 1px solid #E6A23C; padding: 10pt; margin-top: 20pt; text-align: center; }
</style>
</head>
<body>
<h1>${report.reportTitle || '报告'}</h1>
<div class="content">
${report.reportContent || '暂无报告内容'}
</div>
${aiAnalysisHtml}
<div class="disclaimer">
<p style="margin: 0; color: #E6A23C; font-weight: bold;"> 此结果仅供参考不可作为临床诊断的唯一标准</p>
</div>
</body>
</html>
`;
const blob = new Blob(['\ufeff', wordHtml], { type: 'application/msword' });
zip.file(`${safeFileName}.doc`, blob);
} else {
// PDF格式 - 使用jspdf和html2canvas生成真正的PDF
const { default: jsPDF } = await import('jspdf');
const { default: html2canvas } = await import('html2canvas');
// 创建临时容器
const container = document.createElement('div');
container.innerHTML = reportHtml;
container.style.position = 'absolute';
container.style.left = '-9999px';
container.style.width = '794px'; // A4宽度 (210mm 794px at 96dpi)
container.style.padding = '40px';
container.style.backgroundColor = '#fff';
document.body.appendChild(container);
try {
// 使用html2canvas将HTML转换为canvas
const canvas = await html2canvas(container, {
scale: 2,
useCORS: true,
logging: false,
backgroundColor: '#ffffff'
});
// 创建PDF
const pdf = new jsPDF('p', 'mm', 'a4');
const imgData = canvas.toDataURL('image/jpeg', 0.95);
// 计算图片在PDF中的尺寸
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = pdf.internal.pageSize.getHeight();
const imgWidth = pdfWidth - 20; // 留10mm边距
const imgHeight = (canvas.height * imgWidth) / canvas.width;
// 如果内容超过一页需要分页
let heightLeft = imgHeight;
let position = 10; // 顶部边距
// 添加第一页
pdf.addImage(imgData, 'JPEG', 10, position, imgWidth, imgHeight);
heightLeft -= (pdfHeight - 20);
// 添加后续页面
while (heightLeft > 0) {
position = heightLeft - imgHeight + 10;
pdf.addPage();
pdf.addImage(imgData, 'JPEG', 10, position, imgWidth, imgHeight);
heightLeft -= (pdfHeight - 20);
}
// 获取PDF的blob
const pdfBlob = pdf.output('blob');
zip.file(`${safeFileName}.pdf`, pdfBlob);
} finally {
document.body.removeChild(container);
}
}
}
// 生成并下载ZIP文件
const zipBlob = await zip.generateAsync({ type: 'blob' });
const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');
const zipFileName = `报告批量导出_${timestamp}.zip`;
const link = document.createElement('a');
link.href = window.URL.createObjectURL(zipBlob);
link.download = zipFileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(link.href);
this.$message.success(`成功导出 ${this.batchExportReports.length} 个报告`);
this.resetBatchExportDialog();
} catch (error) {
console.error('批量导出失败:', error);
this.$message.error("批量导出失败:" + (error.message || "未知错误"));
} finally {
this.batchExportLoading = false;
}
}
,
extractScaleName(title) {
const raw = (title || '').toString();
let t = raw.replace(/\(次数\d+\)/g, '').trim();
t = t.replace(/(测评报告|答题报告|报告)$/g, '').trim();
t = t.replace(/[\s\-—_]+$/g, '').trim();
return this.sanitizeFilenamePart(t || raw || '报告');
}
,
sanitizeFilenamePart(value) {
const v = (value == null ? '' : String(value)).trim();
if (!v) {
return '-';
}
return v.replace(/[\\/:*?"<>|]/g, '_');
}
}
};
</script>
<style scoped>
.app-container {
padding: 20px;
}
</style>