|
|
|
|
@ -69,123 +69,140 @@
|
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
|
|
<!-- 支付模板字段(fields_data / element_values 双写) -->
|
|
|
|
|
<tr
|
|
|
|
|
v-for="el in visiblePaymentTemplateElements"
|
|
|
|
|
:key="`pay_el_${el.id}`"
|
|
|
|
|
>
|
|
|
|
|
<td class="label">{{ el.name }}</td>
|
|
|
|
|
<td class="value" colspan="3">
|
|
|
|
|
<!-- 勾选清单 -->
|
|
|
|
|
<div v-if="el.type === 'checklist'" class="readonly-checklist">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(opt, idx) in (el.options || [])"
|
|
|
|
|
:key="opt.value || idx"
|
|
|
|
|
v-show="isChecklistChecked(payment?.fields?.[el.id], opt.value) || getChecklistRemark(el, opt.value)"
|
|
|
|
|
class="checklist-option-item"
|
|
|
|
|
>
|
|
|
|
|
<div class="checklist-option-wrapper">
|
|
|
|
|
<el-checkbox
|
|
|
|
|
:model-value="isChecklistChecked(payment?.fields?.[el.id], opt.value)"
|
|
|
|
|
disabled
|
|
|
|
|
class="checklist-checkbox"
|
|
|
|
|
>
|
|
|
|
|
{{ `${idx + 1}、${opt.label}` }}
|
|
|
|
|
</el-checkbox>
|
|
|
|
|
<span
|
|
|
|
|
v-if="getChecklistRemark(el, opt.value)"
|
|
|
|
|
class="checklist-remark-text"
|
|
|
|
|
>
|
|
|
|
|
{{ getChecklistRemark(el, opt.value) }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 会议纪要 -->
|
|
|
|
|
<div v-else-if="el.type === 'meeting_minutes'">
|
|
|
|
|
<MeetingMinutesField
|
|
|
|
|
:model-value="payment?.fields?.[el.id]"
|
|
|
|
|
readonly
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 明细表格 -->
|
|
|
|
|
<div v-else-if="el.type === 'detail_table'" class="readonly-detail-table">
|
|
|
|
|
<el-table
|
|
|
|
|
:data="getDetailTableData(el.id)"
|
|
|
|
|
border
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
size="small"
|
|
|
|
|
>
|
|
|
|
|
<el-table-column type="index" label="#" width="60" align="center" />
|
|
|
|
|
<el-table-column
|
|
|
|
|
v-for="field in getDetailTableFields(el.id)"
|
|
|
|
|
:key="field.field_key"
|
|
|
|
|
:prop="field.field_key"
|
|
|
|
|
:label="field.field_name"
|
|
|
|
|
:min-width="field.field_type === 'textarea' ? 200 : 150"
|
|
|
|
|
<template v-for="el in visiblePaymentTemplateElements" :key="`pay_el_${el.id}`">
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="label">{{ el.name }}</td>
|
|
|
|
|
<td class="value" colspan="3">
|
|
|
|
|
<!-- 勾选清单 -->
|
|
|
|
|
<div v-if="el.type === 'checklist'" class="readonly-checklist">
|
|
|
|
|
<div
|
|
|
|
|
v-for="(opt, idx) in (el.options || [])"
|
|
|
|
|
:key="opt.value || idx"
|
|
|
|
|
v-show="isChecklistChecked(payment?.fields?.[el.id], opt.value) || getChecklistRemark(el, opt.value)"
|
|
|
|
|
class="checklist-option-item"
|
|
|
|
|
>
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<span v-if="field.field_type === 'department'">
|
|
|
|
|
{{ getDepartmentName(row[field.field_key]) }}
|
|
|
|
|
</span>
|
|
|
|
|
<span v-else-if="field.field_type === 'user'">
|
|
|
|
|
{{ getUserName(row[field.field_key]) }}
|
|
|
|
|
</span>
|
|
|
|
|
<span v-else>
|
|
|
|
|
{{ row[field.field_key] || '-' }}
|
|
|
|
|
<div class="checklist-option-wrapper">
|
|
|
|
|
<el-checkbox
|
|
|
|
|
:model-value="isChecklistChecked(payment?.fields?.[el.id], opt.value)"
|
|
|
|
|
disabled
|
|
|
|
|
class="checklist-checkbox"
|
|
|
|
|
>
|
|
|
|
|
{{ `${idx + 1}、${opt.label}` }}
|
|
|
|
|
</el-checkbox>
|
|
|
|
|
<span
|
|
|
|
|
v-if="getChecklistRemark(el, opt.value)"
|
|
|
|
|
class="checklist-remark-text"
|
|
|
|
|
>
|
|
|
|
|
{{ getChecklistRemark(el, opt.value) }}
|
|
|
|
|
</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 审批流程 -->
|
|
|
|
|
<div v-else-if="el.type === 'approval_flow'" class="approval-flow-display">
|
|
|
|
|
<div v-if="getFlowDisplayInfo(el.id)" class="flow-info">
|
|
|
|
|
<el-tag type="primary" size="small">
|
|
|
|
|
{{ getFlowDisplayInfo(el.id) }}
|
|
|
|
|
</el-tag>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span v-else class="muted">-</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 附件类型 -->
|
|
|
|
|
<div v-else-if="el.type === 'form_element' && el.field_type === 'attachment'" class="attachment-display">
|
|
|
|
|
<div v-if="getAttachmentItems(el.id).length" class="file-list">
|
|
|
|
|
<span v-for="(file, idx) in getAttachmentItems(el.id)" :key="`${file.url || file.name}_${idx}`" class="file-item">
|
|
|
|
|
<el-link
|
|
|
|
|
v-if="file.url"
|
|
|
|
|
:href="file.url"
|
|
|
|
|
target="_blank"
|
|
|
|
|
type="primary"
|
|
|
|
|
:underline="false"
|
|
|
|
|
class="file-link"
|
|
|
|
|
@click="handleAttachmentClick(file)"
|
|
|
|
|
>
|
|
|
|
|
{{ file.name || `附件${idx + 1}` }}
|
|
|
|
|
</el-link>
|
|
|
|
|
<el-link
|
|
|
|
|
v-else
|
|
|
|
|
type="primary"
|
|
|
|
|
:underline="false"
|
|
|
|
|
class="file-link file-link--no-url"
|
|
|
|
|
@click="handleAttachmentClick(file)"
|
|
|
|
|
|
|
|
|
|
<!-- 会议纪要 -->
|
|
|
|
|
<div v-else-if="el.type === 'meeting_minutes'">
|
|
|
|
|
<MeetingMinutesField
|
|
|
|
|
:model-value="payment?.fields?.[el.id]"
|
|
|
|
|
readonly
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 明细表格 -->
|
|
|
|
|
<div v-else-if="el.type === 'detail_table'" class="readonly-detail-table">
|
|
|
|
|
<el-table
|
|
|
|
|
:data="getDetailTableData(el.id)"
|
|
|
|
|
border
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
size="small"
|
|
|
|
|
>
|
|
|
|
|
<el-table-column type="index" label="#" width="60" align="center" />
|
|
|
|
|
<el-table-column
|
|
|
|
|
v-for="field in getDetailTableFields(el.id)"
|
|
|
|
|
:key="field.field_key"
|
|
|
|
|
:prop="field.field_key"
|
|
|
|
|
:label="field.field_name"
|
|
|
|
|
:min-width="field.field_type === 'textarea' ? 200 : 150"
|
|
|
|
|
>
|
|
|
|
|
{{ file.name || `附件${idx + 1}` }}
|
|
|
|
|
</el-link>
|
|
|
|
|
<span v-if="idx < getAttachmentItems(el.id).length - 1" class="file-separator">,</span>
|
|
|
|
|
</span>
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
<span v-if="field.field_type === 'department'">
|
|
|
|
|
{{ getDepartmentName(row[field.field_key]) }}
|
|
|
|
|
</span>
|
|
|
|
|
<span v-else-if="field.field_type === 'user'">
|
|
|
|
|
{{ getUserName(row[field.field_key]) }}
|
|
|
|
|
</span>
|
|
|
|
|
<span v-else>
|
|
|
|
|
{{ row[field.field_key] || '-' }}
|
|
|
|
|
</span>
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
|
|
|
|
<span v-else class="muted">-</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 默认展示 -->
|
|
|
|
|
<span v-else>
|
|
|
|
|
{{ formatTemplateFieldValue(el, payment?.fields?.[el.id]) }}
|
|
|
|
|
</span>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<!-- 审批流程 -->
|
|
|
|
|
<div v-else-if="el.type === 'approval_flow'" class="approval-flow-display">
|
|
|
|
|
<div v-if="getFlowDisplayInfo(el.id)" class="flow-info">
|
|
|
|
|
<el-tag type="primary" size="small">
|
|
|
|
|
{{ getFlowDisplayInfo(el.id) }}
|
|
|
|
|
</el-tag>
|
|
|
|
|
</div>
|
|
|
|
|
<span v-else class="muted">-</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 附件类型 -->
|
|
|
|
|
<div v-else-if="el.type === 'form_element' && el.field_type === 'attachment'" class="attachment-display">
|
|
|
|
|
<div v-if="getAttachmentItems(el.id).length" class="file-list">
|
|
|
|
|
<span v-for="(file, idx) in getAttachmentItems(el.id)" :key="`${file.url || file.name}_${idx}`" class="file-item">
|
|
|
|
|
<el-link
|
|
|
|
|
v-if="file.url"
|
|
|
|
|
:href="file.url"
|
|
|
|
|
target="_blank"
|
|
|
|
|
type="primary"
|
|
|
|
|
:underline="false"
|
|
|
|
|
class="file-link"
|
|
|
|
|
@click="handleAttachmentClick(file)"
|
|
|
|
|
>
|
|
|
|
|
{{ file.name || `附件${idx + 1}` }}
|
|
|
|
|
</el-link>
|
|
|
|
|
<el-link
|
|
|
|
|
v-else
|
|
|
|
|
type="primary"
|
|
|
|
|
:underline="false"
|
|
|
|
|
class="file-link file-link--no-url"
|
|
|
|
|
@click="handleAttachmentClick(file)"
|
|
|
|
|
>
|
|
|
|
|
{{ file.name || `附件${idx + 1}` }}
|
|
|
|
|
</el-link>
|
|
|
|
|
<span v-if="idx < getAttachmentItems(el.id).length - 1" class="file-separator">,</span>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<span v-else class="muted">-</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 默认展示 -->
|
|
|
|
|
<span v-else>
|
|
|
|
|
{{ formatTemplateFieldValue(el, payment?.fields?.[el.id]) }}
|
|
|
|
|
</span>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
|
|
<!-- 会议纪要:若唯一附件为 PDF,则在本行后追加一个通栏行展示 PDF(尽量拉宽) -->
|
|
|
|
|
<tr
|
|
|
|
|
v-if="el.type === 'meeting_minutes' && getMeetingMinutesPdfUrl(payment?.fields?.[el.id])"
|
|
|
|
|
class="meeting-minutes-pdf-row"
|
|
|
|
|
>
|
|
|
|
|
<td class="value meeting-minutes-pdf-td" colspan="4">
|
|
|
|
|
<div
|
|
|
|
|
class="print-file-item print-pdf meeting-minutes-pdf"
|
|
|
|
|
:data-pdf-id="`meeting_minutes_${el.id}`"
|
|
|
|
|
:data-pdf-url="getMeetingMinutesPdfUrl(payment?.fields?.[el.id])"
|
|
|
|
|
>
|
|
|
|
|
<div class="pdf-pages-container">
|
|
|
|
|
<div class="pdf-loading">PDF 加载中...</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</template>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
@ -782,10 +799,37 @@ const handlePrint = async (e) => {
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[打印] 处理失败:', error)
|
|
|
|
|
// 即使失败也尝试打印
|
|
|
|
|
window.print()
|
|
|
|
|
window.print()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 会议纪要:识别唯一附件是否为 PDF(字段值可能是对象或 JSON 字符串)
|
|
|
|
|
const getMeetingMinutesPdfUrl = (rawVal) => {
|
|
|
|
|
if (rawVal === null || rawVal === undefined || rawVal === '') return null
|
|
|
|
|
|
|
|
|
|
let v = rawVal
|
|
|
|
|
if (typeof v === 'string') {
|
|
|
|
|
try {
|
|
|
|
|
const parsed = JSON.parse(v)
|
|
|
|
|
if (parsed && typeof parsed === 'object') v = parsed
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!v || typeof v !== 'object') return null
|
|
|
|
|
|
|
|
|
|
const url = (v.file_url || v.url || '').toString().trim()
|
|
|
|
|
if (!url) return null
|
|
|
|
|
|
|
|
|
|
const name = (v.file_name || v.name || '').toString().trim()
|
|
|
|
|
const extFromName = name.includes('.') ? name.split('.').pop() : ''
|
|
|
|
|
const extFromUrl = url.split('?')[0].split('#')[0].split('.').pop()
|
|
|
|
|
const ext = String(extFromName || extFromUrl || '').toLowerCase()
|
|
|
|
|
|
|
|
|
|
return ext === 'pdf' ? url : null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 关闭
|
|
|
|
|
const handleClose = () => {
|
|
|
|
|
// 新窗口打开时可关闭;若用户直接访问打印页,则回退
|
|
|
|
|
@ -968,7 +1012,7 @@ const renderPdfAsImages = async () => {
|
|
|
|
|
await nextTick()
|
|
|
|
|
|
|
|
|
|
// 查找所有 PDF 占位容器
|
|
|
|
|
const pdfContainers = document.querySelectorAll('.flow-template-content .print-pdf[data-pdf-url]')
|
|
|
|
|
const pdfContainers = document.querySelectorAll('.a4-page .print-pdf[data-pdf-url]')
|
|
|
|
|
|
|
|
|
|
for (const container of pdfContainers) {
|
|
|
|
|
const pdfUrl = container.getAttribute('data-pdf-url')
|
|
|
|
|
@ -1003,7 +1047,19 @@ const renderPdfAsImages = async () => {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const arrayBuffer = await response.arrayBuffer()
|
|
|
|
|
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise
|
|
|
|
|
// 配置PDF.js以支持中文字体渲染
|
|
|
|
|
// 注意:如果PDF文件本身没有嵌入字体,中文可能无法正确显示
|
|
|
|
|
// 最佳解决方案是在服务器端处理PDF时确保字体嵌入
|
|
|
|
|
const pdf = await pdfjsLib.getDocument({
|
|
|
|
|
data: arrayBuffer,
|
|
|
|
|
// 启用字体回退,尝试使用系统字体渲染缺失的字符
|
|
|
|
|
standardFontDataUrl: undefined, // 使用默认字体数据
|
|
|
|
|
verbosity: 0, // 减少日志输出
|
|
|
|
|
// 如果PDF使用了CID字体(常用于中文),需要CMap文件
|
|
|
|
|
// 如果项目中有cmaps目录,可以配置cMapUrl和cMapPacked
|
|
|
|
|
// cMapUrl: '/pdfjs/cmaps/',
|
|
|
|
|
// cMapPacked: true
|
|
|
|
|
}).promise
|
|
|
|
|
|
|
|
|
|
// 清空加载提示
|
|
|
|
|
pagesContainer.innerHTML = ''
|
|
|
|
|
@ -1019,10 +1075,18 @@ const renderPdfAsImages = async () => {
|
|
|
|
|
canvas.height = viewport.height
|
|
|
|
|
const context = canvas.getContext('2d')
|
|
|
|
|
|
|
|
|
|
await page.render({
|
|
|
|
|
canvasContext: context,
|
|
|
|
|
viewport
|
|
|
|
|
}).promise
|
|
|
|
|
// 配置渲染选项,确保字体正确渲染
|
|
|
|
|
const renderContext = {
|
|
|
|
|
canvasContext: context,
|
|
|
|
|
viewport: viewport,
|
|
|
|
|
// 启用文本渲染优化
|
|
|
|
|
enableWebGL: false,
|
|
|
|
|
// 字体渲染配置
|
|
|
|
|
transform: null,
|
|
|
|
|
background: null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await page.render(renderContext).promise
|
|
|
|
|
|
|
|
|
|
// 转换为图片
|
|
|
|
|
const img = document.createElement('img')
|
|
|
|
|
@ -1324,10 +1388,18 @@ onMounted(async () => {
|
|
|
|
|
.flow-template-content :deep(.print-file-name) {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
color: #374151;
|
|
|
|
|
color: #409eff;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
padding-bottom: 4px;
|
|
|
|
|
border-bottom: 1px solid #e5e7eb;
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: color 0.2s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.flow-template-content :deep(.print-file-name:hover) {
|
|
|
|
|
color: #66b1ff;
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.flow-template-content :deep(.print-image img) {
|
|
|
|
|
@ -1363,6 +1435,36 @@ onMounted(async () => {
|
|
|
|
|
border: 1px dashed #ddd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 会议纪要等非 flow-template-content 场景也需要 PDF 预览样式(统一挂在 a4-page 下) */
|
|
|
|
|
.a4-page :deep(.print-pdf) {
|
|
|
|
|
width: 100%;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.a4-page :deep(.pdf-pages-container) {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.a4-page :deep(.pdf-page-image) {
|
|
|
|
|
width: 100%;
|
|
|
|
|
display: block;
|
|
|
|
|
page-break-inside: avoid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.a4-page :deep(.pdf-loading),
|
|
|
|
|
.a4-page :deep(.pdf-error) {
|
|
|
|
|
padding: 20px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
color: #666;
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
border: 1px dashed #ddd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.meeting-minutes-pdf-row .meeting-minutes-pdf-td {
|
|
|
|
|
padding-top: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 流程打印模版样式(屏幕) */
|
|
|
|
|
.flow-template-content :deep(table) {
|
|
|
|
|
width: 100%;
|
|
|
|
|
@ -1512,12 +1614,25 @@ onMounted(async () => {
|
|
|
|
|
.flow-template-content :deep(.pdf-error) {
|
|
|
|
|
display: none !important; /* 打印时隐藏加载提示 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.a4-page :deep(.pdf-page-image) {
|
|
|
|
|
width: 100% !important;
|
|
|
|
|
max-width: 100% !important;
|
|
|
|
|
page-break-inside: avoid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.a4-page :deep(.pdf-loading),
|
|
|
|
|
.a4-page :deep(.pdf-error) {
|
|
|
|
|
display: none !important; /* 打印时隐藏加载提示 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 确保文件名在打印时也显示 */
|
|
|
|
|
.flow-template-content :deep(.print-file-name) {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
padding-bottom: 3px;
|
|
|
|
|
color: #409eff;
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|