1362 lines
52 KiB
Vue
1362 lines
52 KiB
Vue
<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>
|
||
|