diff --git a/src/components/MeetingMinutesField.vue b/src/components/MeetingMinutesField.vue
index ab10e00..7439514 100644
--- a/src/components/MeetingMinutesField.vue
+++ b/src/components/MeetingMinutesField.vue
@@ -7,14 +7,18 @@
+
{{ selectedMeetingMinute.title }}
+
{{ selectedMeetingMinute.title }}
props.modelValue, (newVal) => {
inputMode.value = 'oa_meeting_minutes'
selectedMeetingMinute.value = {
id: newVal.meeting_minute_id,
- title: newVal.title || '会议纪要'
+ title: newVal.title || '会议纪要',
+ file_url: newVal.file_url || null
}
// 如果有 meeting_minute_id,加载详情用于展示
if (newVal.meeting_minute_id && props.readonly) {
@@ -721,7 +726,11 @@ const confirmSelect = () => {
return
}
- selectedMeetingMinute.value = selectedRow.value
+ // 初始化 selectedMeetingMinute,确保有 file_url 字段
+ selectedMeetingMinute.value = {
+ ...selectedRow.value,
+ file_url: null // 初始化为 null,后续如果有附件会更新
+ }
const value = {
mode: 'oa_meeting_minutes',
meeting_minute_id: selectedRow.value.id,
@@ -736,6 +745,8 @@ const confirmSelect = () => {
const file = detail.files_details[0]
value.file_url = file.url
value.file_name = file.original_name
+ // 同步更新 selectedMeetingMinute,以便立即显示附件链接
+ selectedMeetingMinute.value.file_url = file.url
}
emit('update:modelValue', value)
})
diff --git a/src/components/PlannedExpenditureTemplateReadonly.vue b/src/components/PlannedExpenditureTemplateReadonly.vue
index 64b078f..f3c4a0c 100644
--- a/src/components/PlannedExpenditureTemplateReadonly.vue
+++ b/src/components/PlannedExpenditureTemplateReadonly.vue
@@ -13,27 +13,25 @@
-
- | {{ field.label || field.name || '-' }} |
-
-
-
-
- {{ getFieldValue(field) }}
-
-
-
- {{ formatNumber(getFieldValue(field)) }}
-
-
-
- {{ formatDate(getFieldValue(field)) }}
-
-
-
+
+
+ | {{ field.label || field.name || '-' }} |
+
+
+
+
+ {{ getFieldValue(field) }}
+
+
+
+ {{ formatNumber(getFieldValue(field)) }}
+
+
+
+ {{ formatDate(getFieldValue(field)) }}
+
+
+ |
-
+ |
+
+
+
+
+ |
+
+ |
+
+
@@ -389,6 +406,35 @@ const getFieldValue = (field) => {
return null
}
+// 会议纪要:识别唯一附件是否为 PDF,并返回 URL(字段值可能是对象或 JSON 字符串)
+const getMeetingMinutesPdfUrl = (field) => {
+ if (!field || field.element_type !== 'meeting_minutes') return null
+ const rawVal = getFieldValue(field)
+ 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 isChecklistChecked = (value, optionValue) => {
if (!value) return false
diff --git a/src/views/payment/ContractManagement.vue b/src/views/payment/ContractManagement.vue
index f1164d5..27b4bbf 100644
--- a/src/views/payment/ContractManagement.vue
+++ b/src/views/payment/ContractManagement.vue
@@ -107,7 +107,6 @@
-
@@ -134,11 +133,19 @@
否
-
+
{{ row.fund_source_budget_data?.name || row.fund_source || '-' }}
+
+
+
+ {{ row.actual_fund_sources.join(' | ') }}
+
+ -
+
+
{{ getDepartmentName(row.owner_department_id) }}
@@ -176,8 +183,9 @@
-
-
+
+ 查看
编辑
删除
@@ -482,6 +490,112 @@
+
+
+
+
+ {{ viewData.contract_no || '-' }}
+ {{ viewData.title || '-' }}
+ {{ viewData.main_content || '-' }}
+ {{ viewData.party_a || '-' }}
+ {{ viewData.party_b || '-' }}
+ {{ formatAmount(viewData.amount_total) }}
+ {{ formatAmount(viewData.budget_amount) }}
+
+ 闭口合同
+ 开口合同
+ -
+
+ {{ viewData.amount_description || '-' }}
+ {{ viewData.sign_date || '-' }}
+ {{ viewData.apply_date || '-' }}
+ {{ viewData.perform_period || '-' }}
+ {{ viewData.pay_method || '-' }}
+
+ {{ viewData.fund_source_year?.year ? viewData.fund_source_year.year + '年' : (viewData.fund_source_budget_data?.budget_year?.year ? viewData.fund_source_budget_data.budget_year.year + '年' : '-') }}
+
+
+ {{ viewData.fund_source_budget_data?.name || viewData.fund_source || '-' }}
+
+
+
+ {{ viewData.actual_fund_sources.join(' | ') }}
+
+ -
+
+
+ {{ viewData.contractType.name }}
+ -
+
+
+ {{ viewData.purchaseCategory.name }}
+ -
+
+
+ {{ viewData.purchaseMethod.name }}
+ -
+
+
+ 是
+ 否
+
+ {{ viewData.tender_agent || '-' }}
+ {{ viewData.perform_status || '-' }}
+
+ 是
+ 否
+
+
+ {{ getDepartmentName(viewData.owner_department_id) }}
+
+
+ {{ formatUserNames(viewData.handler_admin_ids) }}
+
+
+ {{ formatUserNames(viewData.apply_handler_id) }}
+
+
+ {{ getUserName(viewData.purchase_handler_id) }}
+
+
+
+ {{ viewData.payment_stats?.payment_count || 0 }} 次
+ {{ formatAmount(viewData.payment_stats?.paid_amount || 0) }}
+
+
+ {{ viewData.remark || '-' }}
+
+
+
+ {{ viewData.attachment?.original_name || viewData.attachment?.name || '查看' }}
+
+ 附件加载中...
+
+ -
+
+
+
+ 付款计划
+
+
+
+
+
+ {{ formatAmount(row.amount_plan) }}
+
+
+
+
+
+ 关闭
+
+
+
@@ -1065,6 +1179,8 @@ const fundSourceOptions = ref([]) // 项目经费来源选项列表
const purchaseCategoryOptions = ref([]) // 采购类别选项列表
const drawerVisible = ref(false)
+const viewDialogVisible = ref(false)
+const viewData = ref(null)
const formRef = ref(null)
const form = reactive(getEmptyForm())
const payPlanDeleteIds = ref([])
@@ -1991,6 +2107,47 @@ function handlePageChange(val) {
fetchList()
}
+function openViewDialog(row) {
+ if (!row || !row.id) {
+ ElMessage.warning('请选择要查看的合同')
+ return
+ }
+ viewData.value = null
+ viewDialogVisible.value = true
+ loadViewDetail(row.id)
+}
+
+async function loadViewDetail(id) {
+ try {
+ const response = await request.get(`/budget/contracts/${id}`)
+ if (response && response.code === 0) {
+ viewData.value = response.data || response
+ } else {
+ ElMessage.error(response?.msg || '获取详情失败')
+ viewDialogVisible.value = false
+ }
+ } catch (e) {
+ console.error(e)
+ ElMessage.error('获取详情失败:' + (e.message || '未知错误'))
+ viewDialogVisible.value = false
+ }
+}
+
+function getViewAttachmentUrl(row) {
+ if (!row || !row.attachment_id) return null
+
+ if (row.attachment && row.attachment.url) {
+ return row.attachment.url
+ }
+
+ if (row.attachment && row.attachment.folder && row.attachment.name) {
+ const baseUrl = config.baseURL || ''
+ return `${baseUrl}/${row.attachment.folder}/${row.attachment.name}`
+ }
+
+ return null
+}
+
function openForm(row) {
if (!row || !row.id) {
ElMessage.warning('请选择要编辑的合同')
diff --git a/src/views/payment/PaymentDetailPrint.vue b/src/views/payment/PaymentDetailPrint.vue
index fc20147..979834b 100644
--- a/src/views/payment/PaymentDetailPrint.vue
+++ b/src/views/payment/PaymentDetailPrint.vue
@@ -69,123 +69,140 @@
-
- | {{ el.name }} |
-
-
-
-
-
-
- {{ `${idx + 1}、${opt.label}` }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ | {{ el.name }} |
+
+
+
+
-
-
- {{ getDepartmentName(row[field.field_key]) }}
-
-
- {{ getUserName(row[field.field_key]) }}
-
-
- {{ row[field.field_key] || '-' }}
+
+
+ {{ `${idx + 1}、${opt.label}` }}
+
+
-
-
-
-
-
-
-
-
-
- {{ getFlowDisplayInfo(el.id) }}
-
+
+
- -
-
-
-
-
-
-
-
- {{ file.name || `附件${idx + 1}` }}
-
-
+
+
+
+
+
+
+
+
+
- {{ file.name || `附件${idx + 1}` }}
-
- ,
-
+
+
+ {{ getDepartmentName(row[field.field_key]) }}
+
+
+ {{ getUserName(row[field.field_key]) }}
+
+
+ {{ row[field.field_key] || '-' }}
+
+
+
+
- -
-
-
-
- {{ formatTemplateFieldValue(el, payment?.fields?.[el.id]) }}
-
- |
-
+
+
+
+
+ {{ getFlowDisplayInfo(el.id) }}
+
+
+ -
+
+
+
+
+
+
+
+ {{ file.name || `附件${idx + 1}` }}
+
+
+ {{ file.name || `附件${idx + 1}` }}
+
+ ,
+
+
+ -
+
+
+
+
+ {{ formatTemplateFieldValue(el, payment?.fields?.[el.id]) }}
+
+ |
+
+
+
+
+ |
+
+ |
+
+
@@ -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;
}
}