导出样式调整

This commit is contained in:
qweasdzxclm 2026-02-04 18:24:08 +08:00
parent fc595c2c26
commit 6c57427679
2 changed files with 228 additions and 104 deletions

View File

@ -1,20 +1,41 @@
<template>
<Dialog style="display: none;" :title="'评估报告'" v-model="dialogVisible" width="900px">
<Dialog style="display: none" :title="'评估报告'" v-model="dialogVisible" width="900px">
<div v-loading="loading" class="report-edit-container" ref="previewRef">
<template v-if="selectedReport">
<div class="basic-info-title">{{ selectedReport.templateName }}</div>
<!-- 基本信息区 -->
<div class="basic-info-section">
<div class="basic-info-item">服刑人员{{ selectedReport.prisonerName }} ({{ selectedReport.prisonerNo }})</div>
<div class="basic-info-item"
>服刑人员{{ selectedReport.prisonerName }} ({{ selectedReport.prisonerNo }})</div
>
<div class="basic-info-item">监区{{ selectedReport.areaName || '-' }}</div>
<div class="basic-info-item">评估日期{{ formatDateTime(selectedReport.evaluationDate, 'YYYY-MM-DD') }}</div>
<div class="basic-info-item">风险等级{{ getDictLabel(DICT_TYPE.PRISON_RISK_LEVEL, selectedReport.riskLevel) }}</div>
<div class="basic-info-item">状态{{ getDictLabel(DICT_TYPE.PRISON_REPORT_STATUS, selectedReport.status) }}</div>
<div class="basic-info-item"
>评估日期{{ formatDateTime(selectedReport.evaluationDate, 'YYYY-MM-DD') }}</div
>
<div class="basic-info-item"
>风险等级{{
getDictLabel(DICT_TYPE.PRISON_RISK_LEVEL, selectedReport.riskLevel)
}}</div
>
<div class="basic-info-item"
>状态{{ getDictLabel(DICT_TYPE.PRISON_REPORT_STATUS, selectedReport.status) }}</div
>
</div>
<div v-for="item in dimensionAnalysisPanelRef" :key="item.id" class="dimension-item">
<div class="dimension-item-title">{{ item.name }}</div>
<div style="white-space: pre-line; line-height: 1.5;">{{ item.aiAnalysis?.replace(/## 综合分析建议\n\n/g, '') }}</div>
<div
v-for="(item, index) in dimensionAnalysisPanelRef"
:key="item.id"
class="dimension-item"
>
<div class="dimension-item-title"
>{{ ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'][index] }}{{
item.name
}}</div
>
<div style="white-space: pre-line; line-height: 1.5">{{
// '##' CRLF/LF
item.aiAnalysis?.replace(/(^|\r?\n)##.*?\r?\n\r?\n/gm, '$1')
}}</div>
</div>
</template>
@ -32,7 +53,14 @@
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
import { formatDateTime } from '@/utils/formatTime'
import download from '@/utils/download'
import { ReportApi, ReportVO, DimensionDataApi, DimensionDataVO, DimensionApi, DimensionVO } from '@/api/prison/evaluation-report'
import {
ReportApi,
ReportVO,
DimensionDataApi,
DimensionDataVO,
DimensionApi,
DimensionVO
} from '@/api/prison/evaluation-report'
import { PrisonerApi } from '@/api/prison/prisoner'
import { asBlob } from 'html-docx-js-typescript'
import { saveAs } from 'file-saver'
@ -78,7 +106,8 @@ const loadReportDetail = async (id: number) => {
try {
selectedReport.value = await ReportApi.getReport(id)
dimensionDataList.value = await DimensionDataApi.getDimensionDataListByReportId(id)
drawerTitle.value = selectedReport.value?.title || `${selectedReport.value?.prisonerName} - 评估报告`
drawerTitle.value =
selectedReport.value?.title || `${selectedReport.value?.prisonerName} - 评估报告`
//
if (selectedReport.value?.prisonerId && !selectedReport.value.areaName) {
@ -92,15 +121,16 @@ const loadReportDetail = async (id: number) => {
//
if (selectedReport.value?.templateId) {
try {
const dimensionList = await DimensionApi.getDimensionsByTemplateId(selectedReport.value.templateId)
const dimensionList = await DimensionApi.getDimensionsByTemplateId(
selectedReport.value.templateId
)
if (dimensionList && dimensionList.length > 0) {
console.log(dimensionList);
console.log(dimensionList)
dimensions.value = dimensionList
} else {
// 使
dimensions.value = getDefaultDimensions(selectedReport.value.templateId)
}
} catch {
dimensions.value = getDefaultDimensions(selectedReport.value.templateId)
}
@ -108,11 +138,11 @@ const loadReportDetail = async (id: number) => {
if (selectedReport.value?.id && dimensions.value.length > 0) {
const list = await DimensionDataApi.getDimensionDataListByReportId(selectedReport.value.id)
console.log(list, dimensions.value);
dimensionAnalysisPanelRef.value = dimensions.value.map(item => {
console.log(list, dimensions.value)
dimensionAnalysisPanelRef.value = dimensions.value.map((item) => {
return {
...item,
aiAnalysis: list.find(analys => analys.dimensionId === item.id)?.aiAnalysis
aiAnalysis: list.find((analys) => analys.dimensionId === item.id)?.aiAnalysis
}
})
}
@ -127,11 +157,51 @@ const loadReportDetail = async (id: number) => {
/** 获取默认维度配置 */
const getDefaultDimensions = (templateId: number): DimensionVO[] => {
return [
{ id: 1, templateId, name: '基本信息', dimensionType: 1, aiEnabled: 0, status: 0, dataSources: ['prisoner'] },
{ id: 2, templateId, name: '犯罪情况分析', dimensionType: 1, aiEnabled: 1, status: 0, dataSources: ['prisoner', 'risk'] },
{ id: 3, templateId, name: '服刑表现评估', dimensionType: 1, aiEnabled: 1, status: 0, dataSources: ['score', 'violation', 'reward'] },
{ id: 4, templateId, name: '消费行为分析', dimensionType: 1, aiEnabled: 1, status: 0, dataSources: ['consumption'] },
{ id: 5, templateId, name: '综合评估结论', dimensionType: 1, aiEnabled: 1, status: 0, dataSources: ['prisoner', 'psychology'] }
{
id: 1,
templateId,
name: '基本信息',
dimensionType: 1,
aiEnabled: 0,
status: 0,
dataSources: ['prisoner']
},
{
id: 2,
templateId,
name: '犯罪情况分析',
dimensionType: 1,
aiEnabled: 1,
status: 0,
dataSources: ['prisoner', 'risk']
},
{
id: 3,
templateId,
name: '服刑表现评估',
dimensionType: 1,
aiEnabled: 1,
status: 0,
dataSources: ['score', 'violation', 'reward']
},
{
id: 4,
templateId,
name: '消费行为分析',
dimensionType: 1,
aiEnabled: 1,
status: 0,
dataSources: ['consumption']
},
{
id: 5,
templateId,
name: '综合评估结论',
dimensionType: 1,
aiEnabled: 1,
status: 0,
dataSources: ['prisoner', 'psychology']
}
]
}
@ -189,40 +259,36 @@ const exportToWord = async () => {
<head>
<meta charset="utf-8">
<style>
/* 使用 pt 单位以避免 Word/WPS 在 px->pt 换算时产生差异 */
body {
font-family: 'Microsoft YaHei', '微软雅黑', SimSun, Arial, sans-serif;
font-size: 14px;
line-height: 1.6;
margin: 20px;
padding: 20px;
font-size: 11pt;
line-height: 1.5;
}
.basic-info-title{
font-family: '黑体', 'Microsoft YaHei', SimSun, Arial, sans-serif;
font-size: 21px;
font-size: 18pt;
font-weight: 700;
color: black;
text-align: center;
margin-bottom: 20px;
margin-bottom: 15pt;
}
.basic-info-section {
padding: 15px 20px;
color: black;
font-size: 12px;
font-size: 11pt;
}
.basic-info-item{
margin-right: 30px;
margin-right: 25pt;
}
.dimension-item {
padding: 0 40px;
font-size: 12px;
font-size: 11pt;
color: black;
}
.dimension-item-title {
font-size: 21px;
padding: 15px 0;
font-size: 15pt;
font-weight: 500;
color: black;
margin-top: 15pt;
}
</style>
</head>
@ -269,7 +335,7 @@ defineExpose({ open })
color: black;
font-size: 14px;
}
.basic-info-item{
.basic-info-item {
margin-right: 30px;
}

View File

@ -1,5 +1,11 @@
<template>
<Dialog style="display: none;" title="问卷预览" v-model="dialogVisible" width="900px" :fullscreen="false">
<Dialog
style="display: none"
title="问卷预览"
v-model="dialogVisible"
width="900px"
:fullscreen="false"
>
<div ref="previewRef" class="questionnaire-preview" v-loading="loading">
<!-- 问卷头部信息 -->
<h1 class="preview-header">{{ recordInfo?.questionnaireName }}</h1>
@ -18,15 +24,16 @@
<!-- 问题列表按分区显示 -->
<div class="preview-questions">
<template v-for="partition in partitions" :key="partition.name || 'default'">
<template v-for="(partition, index) in partitions" :key="partition.name || 'default'">
<!-- 分区标题 -->
<div v-if="partition.name" class="partition-title">
{{ ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'][index] }}
{{ partition.name }}
<span class="question-count">({{ partition.questions.length }} 道题)</span>
</div>
<!-- 问题列表 -->
<div class="question-items">
<!-- 问题列表 -->
<div class="question-items">
<div
v-for="(questionWithAnswer, index) in partition.questions"
:key="questionWithAnswer.question.id"
@ -41,25 +48,50 @@
</span>
<!-- 单选/多选题 -->
<span v-if="questionWithAnswer.question.type === 1 || questionWithAnswer.question.type === 2" class="question-options-inline">
<span
v-if="
questionWithAnswer.question.type === 1 || questionWithAnswer.question.type === 2
"
class="question-options-inline"
>
<span
v-for="option in getQuestionOptions(questionWithAnswer.question)"
:key="option.label"
class="option-item"
>
<span v-if="questionWithAnswer.question.type === 1 && questionWithAnswer.answer?.answerText?.trim() === option.label">
<span
v-if="
questionWithAnswer.question.type === 1 &&
questionWithAnswer.answer?.answerText?.trim() === option.label
"
>
{{ option.label }}
</span>
<span v-if="questionWithAnswer.question.type === 1 && questionWithAnswer.answer?.answerText?.trim() !== option.label">
<span
v-if="
questionWithAnswer.question.type === 1 &&
questionWithAnswer.answer?.answerText?.trim() !== option.label
"
>
{{ option.label }}
</span>
<span v-if="questionWithAnswer.question.type === 2 && getSelectedLabels(questionWithAnswer.answer).includes(option.label)">
<span
v-if="
questionWithAnswer.question.type === 2 &&
getSelectedLabels(questionWithAnswer.answer).includes(option.label)
"
>
{{ option.label }}
</span>
<span v-if="questionWithAnswer.question.type === 2 && !getSelectedLabels(questionWithAnswer.answer).includes(option.label)">
<span
v-if="
questionWithAnswer.question.type === 2 &&
!getSelectedLabels(questionWithAnswer.answer).includes(option.label)
"
>
{{ option.label }}
</span>
@ -67,26 +99,48 @@
</span>
<!-- 填空题 -->
<span v-else-if="questionWithAnswer.question.type === 3" class="question-input-inline">
<span
v-else-if="questionWithAnswer.question.type === 3"
class="question-input-inline"
>
{{ getAnswerDisplayValue(questionWithAnswer.answer) }}
</span>
<!-- 评分题 -->
<span v-else-if="questionWithAnswer.question.type === 4" class="question-rating-inline">
<span class="question-input-inline">{{ getAnswerDisplayValue(questionWithAnswer.answer) }}</span>
<span
v-else-if="questionWithAnswer.question.type === 4"
class="question-rating-inline"
>
<span class="question-input-inline">{{
getAnswerDisplayValue(questionWithAnswer.answer)
}}</span>
</span>
<!-- 日期题 -->
<span v-else-if="questionWithAnswer.question.type === 5" class="question-date-inline">
<span class="date-info" v-if="getRangeValue(questionWithAnswer.question, 'min') || getRangeValue(questionWithAnswer.question, 'max')">
日期范围{{ getRangeValue(questionWithAnswer.question, 'min') || '无限制' }} ~ {{ getRangeValue(questionWithAnswer.question, 'max') || '无限制' }}
<span
class="date-info"
v-if="
getRangeValue(questionWithAnswer.question, 'min') ||
getRangeValue(questionWithAnswer.question, 'max')
"
>
日期范围{{ getRangeValue(questionWithAnswer.question, 'min') || '无限制' }} ~
{{ getRangeValue(questionWithAnswer.question, 'max') || '无限制' }}
</span>
<span class="question-input-inline">{{ getAnswerDisplayValue(questionWithAnswer.answer) }}</span>
<span class="question-input-inline">{{
getAnswerDisplayValue(questionWithAnswer.answer)
}}</span>
</span>
<!-- 数字题 -->
<span v-else-if="questionWithAnswer.question.type === 6" class="question-number-inline">
<span class="question-input-inline">{{ getAnswerDisplayValue(questionWithAnswer.answer) }}</span>
<span
v-else-if="questionWithAnswer.question.type === 6"
class="question-number-inline"
>
<span class="question-input-inline">{{
getAnswerDisplayValue(questionWithAnswer.answer)
}}</span>
</span>
</div>
</div>
@ -137,7 +191,7 @@ const partitions = computed(() => {
questions.value.forEach((q, index) => {
const partName = q.partName || ''
const answer = answers.value.find(a => a.questionId === q.id)
const answer = answers.value.find((a) => a.questionId === q.id)
if (!partMap.has(partName)) {
partMap.set(partName, [])
@ -150,12 +204,11 @@ const partitions = computed(() => {
})
//
const sortedParts = Array.from(partMap.entries())
.sort((a, b) => {
const sortA = a[1][0]?.question.partSort ?? 0
const sortB = b[1][0]?.question.partSort ?? 0
return sortA - sortB
})
const sortedParts = Array.from(partMap.entries()).sort((a, b) => {
const sortA = a[1][0]?.question.partSort ?? 0
const sortB = b[1][0]?.question.partSort ?? 0
return sortA - sortB
})
//
const result: Array<{ name: string; questions: QuestionWithAnswer[] }> = []
@ -184,7 +237,7 @@ const partitions = computed(() => {
/** 根据问题ID获取答案 */
const getAnswerByQuestionId = (questionId: number): Answer | undefined => {
return answers.value.find(a => a.questionId === questionId)
return answers.value.find((a) => a.questionId === questionId)
}
/** 获取答案显示值(兼容不同字段名) */
@ -197,7 +250,7 @@ const getAnswerDisplayValue = (answer?: Answer): string => {
/** 问卷类型标签 */
const getTypeLabel = (type: number) => {
const options = getIntDictOptions(DICT_TYPE.PRISON_QUESTIONNAIRE_TYPE)
return options.find(o => o.value === type)?.label || '未知'
return options.find((o) => o.value === type)?.label || '未知'
}
/** 获取问题选项 */
@ -233,11 +286,16 @@ const getRangeValue = (question: Question, key: 'min' | 'max') => {
/** 安全获取多选答案的标签数组 */
const getSelectedLabels = (answer?: Answer): string[] => {
if (!answer?.answerText) return []
return answer.answerText.split(',').map(s => s.trim())
return answer.answerText.split(',').map((s) => s.trim())
}
/** 判断选项是否被选中(支持单选和多选)- 已废弃,改用模板直接绑定 */
const isOptionSelected = (answer?: Answer, optionValue?: string, optionLabel?: string, questionType?: number): boolean => {
const isOptionSelected = (
answer?: Answer,
optionValue?: string,
optionLabel?: string,
questionType?: number
): boolean => {
if (!answer || !optionValue || !optionLabel) {
return false
}
@ -245,7 +303,7 @@ const isOptionSelected = (answer?: Answer, optionValue?: string, optionLabel?: s
// type === 2answerText
if (questionType === 2 && answer.answerText && answer.answerText.includes(',')) {
const answerText = answer.answerText
const selectedLabels = answerText.split(',').map(s => s.trim())
const selectedLabels = answerText.split(',').map((s) => s.trim())
return selectedLabels.includes(optionLabel)
}
@ -257,7 +315,7 @@ const isOptionSelected = (answer?: Answer, optionValue?: string, optionLabel?: s
// questionType
if (answer.answerText && answer.answerText.includes(',')) {
const answerText = answer.answerText
const selectedLabels = answerText.split(',').map(s => s.trim())
const selectedLabels = answerText.split(',').map((s) => s.trim())
return selectedLabels.includes(optionLabel)
}
@ -266,7 +324,6 @@ const isOptionSelected = (answer?: Answer, optionValue?: string, optionLabel?: s
return false
}
/** 打开弹窗 */
const open = async (recordId: number) => {
dialogVisible.value = true
@ -296,7 +353,9 @@ const open = async (recordId: number) => {
if (recordData.questionnaireId) {
try {
const { QuestionnaireApi } = await import('@/api/prison/questionnaire')
questionnaireInfo.value = await QuestionnaireApi.getQuestionnaire(recordData.questionnaireId)
questionnaireInfo.value = await QuestionnaireApi.getQuestionnaire(
recordData.questionnaireId
)
} catch (error) {
console.warn('加载问卷详细信息失败:', error)
}
@ -336,108 +395,108 @@ const exportToWord = async () => {
<meta charset="utf-8">
<title>${recordInfo.value?.questionnaireName || '问卷'}</title>
<style>
/* 使用 pt 单位以避免 Word/WPS 在 px->pt 换算时产生差异 */
body {
font-family: 'Microsoft YaHei', '微软雅黑', SimSun, Arial, sans-serif;
font-size: 16px;
line-height: 1.6;
margin: 20px;
padding: 20px;
font-size: 11pt;
line-height: 1.5;
margin: 15pt;
padding: 15pt;
}
h1 {
font-family: 'Microsoft YaHei', '微软雅黑', SimSun, Arial, sans-serif;
font-size: 18pt;
font-weight: 500;
color: #000;
text-align: center;
}
.section-title {
margin: 15px 0;
font-size: 18px;
font-weight: 500;
font-family: 'Microsoft YaHei', '微软雅黑', SimSun, Arial, sans-serif;
margin-bottom: 15pt;
font-size: 18pt;
font-weight: 700;
color: #000;
}
.description-content {
color: #000;
line-height: 1.8;
font-size: 16px;
line-height: 1.5;
font-size: 11pt;
}
.instruction-content {
color: #000;
font-size: 18px;
font-size: 11pt;
font-weight: 500;
line-height: 1.8;
line-height: 1.5;
}
.partition-title {
margin-bottom: 16px;
margin-top: 24px;
margin-top: 15pt;
color: #000;
font-size: 17px;
font-size: 15pt;
font-weight: 500;
}
.partition-title:first-child {
margin-top: 0;
}
.question-count {
font-size: 14px;
font-size: 11pt;
color: #909399;
font-weight: normal;
margin-left: 4px;
margin-left: 2pt;
}
.question-item {
font-size: 16px;
font-size: 11pt;
color: #000;
line-height: 1.6;
margin-bottom: 16px;
line-height: 1.5;
page-break-inside: avoid;
}
.question-index {
margin-right: 4px;
font-weight: bold;
margin-right: 2pt;
}
.question-title {
margin-right: 8px;
margin-right: 4pt;
}
.question-help-inline {
display: inline-flex;
align-items: center;
gap: 6px;
gap: 3pt;
color: #909399;
font-size: 15px;
padding: 4px 8px;
font-size: 11pt;
background: #f4f4f5;
border-radius: 4px;
margin-right: 8px;
border-radius: 2pt;
margin-right: 6pt;
}
.question-options-inline {
display: inline-flex;
flex-wrap: wrap;
gap: 12px;
gap: 8pt;
align-items: center;
}
.option-item {
display: inline-flex;
align-items: center;
gap: 8px;
gap: 3pt;
}
.question-rating-inline,
.question-date-inline,
.question-number-inline {
display: inline-flex;
align-items: center;
gap: 12px;
gap: 8pt;
flex-wrap: wrap;
margin-left: 8px;
margin-left: 3pt;
}
.rating-info,
.date-info,
.number-info {
display: inline-flex;
gap: 16px;
gap: 11pt;
color: #909399;
font-size: 13px;
font-size: 11pt;
}
.question-input-inline {
color: #000;
font-size: 16px;
line-height: 2.2;
font-size: 11pt;
line-height: 1.5;
text-decoration: underline;
}
</style>
@ -521,7 +580,6 @@ h1 {
}
.question-index {
margin-right: 4px;
font-weight: bold;
}
.question-title {
margin-right: 8px;