会议纪要多选,关联流程弹窗中附件json展示调整

master
weizong song 2 months ago
parent 650036ef7f
commit e4d7bcaae0

@ -3,59 +3,26 @@
<!-- 录入态 -->
<div v-if="!readonly" class="input-container">
<!-- 已选择/已上传的展示区域 -->
<div v-if="selectedMeetingMinute || uploadFileList.length > 0" class="selected-display">
<!-- 已选择会议纪要 -->
<div v-if="selectedMeetingMinute" class="selected-item selected-oa">
<el-icon class="item-icon"><Document /></el-icon>
<!-- 如果有附件URL,链接到附件;否则显示普通文本 -->
<el-link
v-if="selectedMeetingMinute.file_url"
type="primary"
:underline="false"
class="item-title item-title--link"
:href="selectedMeetingMinute.file_url"
target="_blank"
>
{{ selectedMeetingMinute.title }}
<div v-if="selectedItems.length > 0" class="selected-display">
<div
v-for="(item, index) in selectedItems"
:key="getItemKey(item, index)"
class="selected-item"
:class="item.mode === 'oa_meeting_minutes' ? 'selected-oa' : 'selected-upload'"
>
<el-icon class="item-icon"><Document v-if="item.mode === 'oa_meeting_minutes'" /><Paperclip v-else /></el-icon>
<el-link v-if="item.file_url || item.url" type="primary" :underline="false" class="item-title item-title--link" :href="item.file_url || item.url" target="_blank">
{{ item.title || item.name || '会议纪要' }}
</el-link>
<span v-else class="item-title">{{ selectedMeetingMinute.title }}</span>
<el-button
type="primary"
link
size="small"
@click="handleViewSelectedDetail"
class="item-view"
>
查看详情
</el-button>
<el-button
type="danger"
link
size="small"
@click="clearSelection"
class="item-remove"
>
<el-icon><Close /></el-icon>
</el-button>
</div>
<!-- 已上传文件 -->
<div v-else-if="uploadFileList.length > 0" class="selected-item selected-upload">
<el-icon class="item-icon"><Paperclip /></el-icon>
<span class="item-title">{{ uploadFileList[0].name }}</span>
<el-button
type="danger"
link
size="small"
@click="handleUploadRemove"
class="item-remove"
>
<el-icon><Close /></el-icon>
</el-button>
<span v-else class="item-title">{{ item.title || item.name || '会议纪要' }}</span>
<el-button v-if="item.mode === 'oa_meeting_minutes'" type="primary" link size="small" @click="handleViewSelectedDetail(item)">查看详情</el-button>
<el-button type="primary" link size="small" :disabled="index === 0" @click="moveItem(index, -1)">上移</el-button>
<el-button type="primary" link size="small" :disabled="index === selectedItems.length - 1" @click="moveItem(index, 1)">下移</el-button>
<el-button type="danger" link size="small" @click="removeItem(index)" class="item-remove"><el-icon><Close /></el-icon></el-button>
</div>
</div>
<!-- 未选择时的操作区域 -->
<div v-else class="action-container">
<div class="action-container">
<!-- 模式选择 -->
<el-radio-group v-model="inputMode" class="mode-selector" @change="handleModeChange" size="small">
<el-radio-button label="oa_meeting_minutes">选择会议纪要</el-radio-button>
@ -69,7 +36,7 @@
type="primary"
size="small"
@click="showSelectDialog = true"
:disabled="disabled"
:disabled="disabled || selectedItems.length >= maxItems"
>
<el-icon><Document /></el-icon>
选择
@ -80,11 +47,9 @@
:headers="uploadHeaders"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:on-remove="handleUploadRemove"
:before-upload="beforeUpload"
:file-list="uploadFileList"
:limit="1"
:disabled="disabled"
:file-list="[]"
:disabled="disabled || selectedItems.length >= maxItems"
:show-file-list="false"
>
<el-button type="primary" size="small" :disabled="disabled">
@ -98,25 +63,24 @@
<!-- 展示态 -->
<div v-else class="readonly-display">
<div v-if="titleOnly && displayValue" class="readonly-item readonly-duplicate">
<span class="readonly-title">{{ duplicateTitle }}</span>
<span v-if="repeatNotice" class="readonly-repeat-note">{{ repeatNotice }}</span>
</div>
<!-- 关联 OA 会议纪要 -->
<div v-else-if="displayValue && displayValue.mode === 'oa_meeting_minutes'" class="readonly-item readonly-oa">
<div v-for="(item, index) in displayItems" :key="getItemKey(item, index)" class="readonly-item" :class="item.mode === 'oa_meeting_minutes' ? 'readonly-oa' : 'readonly-upload'">
<template v-if="titleOnly || getItemUsage(item, index).isDuplicate">
<span class="readonly-title">{{ item.title || item.name || '会议纪要' }}</span>
<span v-if="repeatNotice" class="readonly-repeat-note">{{ repeatNotice }}</span>
</template>
<template v-else-if="item.mode === 'oa_meeting_minutes'">
<el-link
type="primary"
:underline="false"
class="readonly-title readonly-title--link"
@click.prevent="handleViewDetail"
>
{{ displayValue.title || '会议纪要' }}
{{ item.title || '会议纪要' }}
</el-link>
<div class="readonly-actions">
<el-link
v-if="displayValue.file_url"
:href="displayValue.file_url"
v-if="item.file_url"
:href="item.file_url"
target="_blank"
type="primary"
size="small"
@ -125,15 +89,13 @@
下载
</el-link>
</div>
</div>
<!-- 上传的附件 -->
<div v-else-if="displayValue && displayValue.mode === 'upload'" class="readonly-item readonly-upload">
</template>
<template v-else-if="item.mode === 'upload'">
<el-icon class="readonly-icon"><Paperclip /></el-icon>
<span class="readonly-title">{{ displayValue.name || '附件' }}</span>
<span class="readonly-title">{{ item.name || '附件' }}</span>
<el-link
v-if="displayValue.url"
:href="displayValue.url"
v-if="item.url"
:href="item.url"
target="_blank"
type="primary"
size="small"
@ -142,10 +104,11 @@
<el-icon><Download /></el-icon>
下载
</el-link>
</template>
</div>
<!-- 空状态 -->
<div v-else class="readonly-empty">
<div v-if="displayItems.length === 0" class="readonly-empty">
<span class="empty-text">未选择</span>
</div>
</div>
@ -179,11 +142,10 @@
:data="meetingMinutesList"
v-loading="loadingMeetingMinutes"
@expand-change="handleExpandChange"
@row-click="handleSelectMeetingMinute"
:row-class-name="getRowClassName"
highlight-current-row
@selection-change="handleSelectionChange"
style="width: 100%; margin-top: 16px"
>
<el-table-column type="selection" width="48" />
<!-- 显眼的展开入口:按钮列(不依赖小三角) -->
<el-table-column label="详情" width="110" align="center" class-name="expand-action-col">
<template #default="{ row }">
@ -344,7 +306,7 @@
</div>
<template #footer>
<el-button @click="showSelectDialog = false">取消</el-button>
<el-button type="primary" @click="confirmSelect" :disabled="!selectedRow">确定</el-button>
<el-button type="primary" @click="confirmSelect" :disabled="selectedRows.length === 0">确定</el-button>
</template>
</el-dialog>
@ -484,7 +446,7 @@
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Document, Upload, Download, Paperclip, Search, Close, List } from '@element-plus/icons-vue'
import { oaMeetingMinutesAPI } from '@/utils/api'
import { getToken } from '@/utils/auth'
@ -492,7 +454,7 @@ import config from '@/config'
const props = defineProps({
modelValue: {
type: [Object, String, null],
type: [Array, Object, String, null],
default: null
},
readonly: {
@ -514,6 +476,14 @@ const props = defineProps({
repeatNotice: {
type: String,
default: ''
},
itemUsageResolver: {
type: Function,
default: null
},
maxItems: {
type: Number,
default: 20
}
})
@ -527,8 +497,7 @@ const showSelectDialog = ref(false)
const meetingMinutesList = ref([])
const loadingMeetingMinutes = ref(false)
const searchKeyword = ref('')
const selectedRow = ref(null)
const selectedMeetingMinute = ref(null)
const selectedRows = ref([])
const meetingTableRef = ref(null)
const pagination = ref({
currentPage: 1,
@ -537,7 +506,6 @@ const pagination = ref({
})
// 上传相关
const uploadFileList = ref([])
const uploadAction = computed(() => {
const baseURL = config.api.baseURL
return `${baseURL}/upload-file`
@ -557,28 +525,25 @@ const expandedRowDetails = ref({})
const expandedRowLoading = ref({})
// 显示值(用于 readonly 模式)
const displayValue = computed(() => {
if (!props.modelValue) return null
if (typeof props.modelValue === 'string') {
// 兼容:字符串可能是 JSON(后端 value_text)
try {
const parsed = JSON.parse(props.modelValue)
if (parsed && typeof parsed === 'object') return parsed
} catch {
// ignore
}
return null
const normalizeMeetingMinutesValue = (raw) => {
if (!raw) return []
if (typeof raw === 'string') {
try { raw = JSON.parse(raw) } catch { return [] }
}
if (typeof props.modelValue === 'object') {
return props.modelValue
}
return null
})
if (!Array.isArray(raw)) raw = raw && typeof raw === 'object' ? [raw] : []
return raw.filter(item => item && typeof item === 'object').map((item, index) => ({ ...item, sort_order: index }))
}
const duplicateTitle = computed(() => {
if (!displayValue.value || typeof displayValue.value !== 'object') return '会议纪要'
return displayValue.value.title || displayValue.value.name || '会议纪要'
})
const selectedItems = ref([])
const displayItems = computed(() => normalizeMeetingMinutesValue(props.modelValue))
const getItemKey = (item, index) => `${item.mode || 'unknown'}:${item.meeting_minute_id || item.upload_id || index}`
const getItemUsage = (item, index) => typeof props.itemUsageResolver === 'function'
? props.itemUsageResolver(item, index)
: { isDuplicate: false }
const emitItems = (items) => {
selectedItems.value = items.map((item, index) => ({ ...item, sort_order: index }))
emit('update:modelValue', selectedItems.value)
}
// 拉取会议纪要详情(不直接绑定到 UI,返回 data)
const fetchMeetingMinuteDetail = async (id) => {
@ -610,46 +575,17 @@ const loadMeetingMinuteDetailToDialog = async (id) => {
// 初始化:根据 modelValue 设置模式和选中项
watch(() => props.modelValue, (newVal) => {
if (!newVal) {
inputMode.value = 'oa_meeting_minutes'
selectedMeetingMinute.value = null
uploadFileList.value = []
return
}
if (typeof newVal === 'object') {
if (newVal.mode === 'oa_meeting_minutes') {
inputMode.value = 'oa_meeting_minutes'
selectedMeetingMinute.value = {
id: newVal.meeting_minute_id,
title: newVal.title || '会议纪要',
file_url: newVal.file_url || null
}
// 如果有 meeting_minute_id,加载详情用于展示
if (newVal.meeting_minute_id && props.readonly) {
loadMeetingMinuteDetailToDialog(newVal.meeting_minute_id)
}
} else if (newVal.mode === 'upload') {
inputMode.value = 'upload'
if (newVal.upload_id) {
uploadFileList.value = [{
name: newVal.name || '附件',
url: newVal.url,
uid: Date.now()
}]
}
}
}
}, { immediate: true })
const items = normalizeMeetingMinutesValue(newVal)
selectedItems.value = items
inputMode.value = items[0]?.mode || 'oa_meeting_minutes'
}, { immediate: true, deep: true })
// 模式切换
const handleModeChange = (mode) => {
if (mode === 'oa_meeting_minutes') {
uploadFileList.value = []
emit('update:modelValue', null)
} else {
selectedMeetingMinute.value = null
emit('update:modelValue', null)
if (selectedItems.value.length > 0 && selectedItems.value[0].mode !== mode) {
ElMessageBox.confirm('切换来源会清空已添加的会议纪要,是否继续?', '确认切换', { type: 'warning' })
.then(() => emitItems([]))
.catch(() => { inputMode.value = selectedItems.value[0].mode })
}
}
@ -708,18 +644,7 @@ const handleSearch = () => {
}
// 选择行
const handleSelectMeetingMinute = (row) => {
selectedRow.value = row
// 同步 Element Plus 当前行高亮
if (meetingTableRef.value && typeof meetingTableRef.value.setCurrentRow === 'function') {
meetingTableRef.value.setCurrentRow(row)
}
}
const getRowClassName = ({ row }) => {
if (selectedRow.value && row && row.id === selectedRow.value.id) return 'row-selected-strong'
return ''
}
const handleSelectionChange = (rows) => { selectedRows.value = rows || [] }
const toggleExpandRow = async (row) => {
if (!meetingTableRef.value || typeof meetingTableRef.value.toggleRowExpansion !== 'function') return
@ -738,56 +663,49 @@ const toggleExpandRow = async (row) => {
}
// 确认选择
const confirmSelect = () => {
if (!selectedRow.value) {
ElMessage.warning('请选择一条会议纪要')
const confirmSelect = async () => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请至少选择一条会议纪要')
return
}
// 初始化 selectedMeetingMinute,确保有 file_url 字段
selectedMeetingMinute.value = {
...selectedRow.value,
file_url: null // 初始化为 null,后续如果有附件会更新
}
const value = {
mode: 'oa_meeting_minutes',
meeting_minute_id: selectedRow.value.id,
title: selectedRow.value.title
}
// 如果有附件,添加文件信息
if (selectedRow.value.files && selectedRow.value.files.length > 0) {
// 加载详情获取文件信息
fetchMeetingMinuteDetail(selectedRow.value.id).then((detail) => {
if (detail && detail.files_details && detail.files_details.length > 0) {
const file = detail.files_details[0]
value.file_url = file.url
value.file_name = file.original_name
// 同步更新 selectedMeetingMinute,以便立即显示附件链接
selectedMeetingMinute.value.file_url = file.url
const existingIds = new Set(selectedItems.value.map(item => item.meeting_minute_id))
const additions = await Promise.all(selectedRows.value
.filter(row => !existingIds.has(row.id))
.map(async (row) => {
const detail = await fetchMeetingMinuteDetail(row.id)
const file = detail?.files_details?.[0]
return {
mode: 'oa_meeting_minutes',
meeting_minute_id: row.id,
title: row.title,
...(file ? { file_url: file.url, file_name: file.original_name || file.name } : {})
}
emit('update:modelValue', value)
})
} else {
emit('update:modelValue', value)
}
}))
const items = [...selectedItems.value, ...additions].slice(0, props.maxItems)
if (items.length === selectedItems.value.length && additions.length > 0) ElMessage.warning(`最多关联 ${props.maxItems} 条会议纪要`)
emitItems(items)
selectedRows.value = []
showSelectDialog.value = false
}
// 清除选择
const clearSelection = () => {
selectedMeetingMinute.value = null
emit('update:modelValue', null)
const removeItem = (index) => emitItems(selectedItems.value.filter((_, itemIndex) => itemIndex !== index))
const moveItem = (index, offset) => {
const targetIndex = index + offset
if (targetIndex < 0 || targetIndex >= selectedItems.value.length) return
const items = [...selectedItems.value]
;[items[index], items[targetIndex]] = [items[targetIndex], items[index]]
emitItems(items)
}
// 查看详情
const handleViewDetail = async () => {
if (!displayValue.value || !displayValue.value.meeting_minute_id) return
const item = displayItems.value[0]
if (!item?.meeting_minute_id) return
showDetailDialog.value = true
const detail = await loadMeetingMinuteDetailToDialog(displayValue.value.meeting_minute_id)
const detail = await loadMeetingMinuteDetailToDialog(item.meeting_minute_id)
if (!detail) {
ElMessage.warning('未获取到会议纪要详情')
@ -796,8 +714,8 @@ const handleViewDetail = async () => {
}
// 录入态:查看“已选择”的会议纪要详情
const handleViewSelectedDetail = async () => {
const id = selectedMeetingMinute.value?.id
const handleViewSelectedDetail = async (item) => {
const id = item?.meeting_minute_id
if (!id) return
showDetailDialog.value = true
const detail = await loadMeetingMinuteDetailToDialog(id)
@ -897,15 +815,22 @@ const handleUploadSuccess = (response) => {
url: response.data?.url || response.data?.path || response.data,
uid: Date.now() + Math.random()
}
uploadFileList.value = [fileInfo]
const value = {
mode: 'upload',
upload_id: response.data?.id || response.data?.id,
upload_id: response.data?.id,
name: fileInfo.name,
url: fileInfo.url
}
emit('update:modelValue', value)
const existingIds = new Set(selectedItems.value.map(item => item.upload_id))
if (existingIds.has(value.upload_id)) {
ElMessage.warning('该附件已添加')
return
}
if (selectedItems.value.length >= props.maxItems) {
ElMessage.warning(`最多关联 ${props.maxItems} 条会议纪要`)
return
}
emitItems([...selectedItems.value, value])
ElMessage.success('上传成功')
} else {
ElMessage.error('上传失败:' + (response.msg || '未知错误'))
@ -919,11 +844,6 @@ const handleUploadError = (error) => {
}
// 移除文件
const handleUploadRemove = () => {
uploadFileList.value = []
emit('update:modelValue', null)
}
// 打开选择对话框时加载列表
watch(showSelectDialog, (visible) => {
if (visible) {
@ -944,6 +864,13 @@ watch(showSelectDialog, (visible) => {
/* 已选择/已上传的展示 */
.selected-display {
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
}
.selected-display + .action-container {
margin-top: 10px;
}
.selected-item {

@ -78,7 +78,8 @@
:model-value="getFieldValue(field)"
readonly
:title-only="getMeetingMinutesUsageState(field).isDuplicate"
:repeat-notice="getMeetingMinutesUsageState(field).isDuplicate ? REPEATED_MEETING_MINUTES_MESSAGE : ''"
:repeat-notice="REPEATED_MEETING_MINUTES_MESSAGE"
:item-usage-resolver="(item, index) => getMeetingMinutesItemUsage(field, item, index)"
/>
</div>
<!-- 明细表格类型(卡片渲染) -->
@ -201,16 +202,17 @@
</td>
</tr>
<!-- 会议纪要:若唯一附件为 PDF,则在本行后追加一个通栏行展示 PDF(尽量拉宽) -->
<!-- 会议纪要数组中的每个 PDF 单独输出。 -->
<tr
v-if="field.element_type === 'meeting_minutes' && !getMeetingMinutesUsageState(field).isDuplicate && getMeetingMinutesPdfUrl(field)"
v-for="(pdf, itemIndex) in (field.element_type === 'meeting_minutes' ? getMeetingMinutesPdfItems(field) : [])"
:key="`planned-meeting-pdf-${field.element_id || field.key}-${itemIndex}`"
class="meeting-minutes-pdf-row"
>
<td class="value meeting-minutes-pdf-td" colspan="2">
<div
class="print-file-item print-pdf meeting-minutes-pdf"
:data-pdf-id="`planned_meeting_minutes_${field.element_id || field.key}`"
:data-pdf-url="getMeetingMinutesPdfUrl(field)"
:data-pdf-id="`planned_meeting_minutes_${field.element_id || field.key}_${itemIndex}`"
:data-pdf-url="pdf"
>
<div class="pdf-pages-container">
<div class="pdf-loading">PDF 加载中...</div>
@ -446,6 +448,29 @@ const getMeetingMinutesPdfUrl = (field) => {
return ext === 'pdf' ? url : null
}
const getMeetingMinutesPdfItems = (field) => {
const rawVal = getFieldValue(field)
let parsed = rawVal
if (typeof parsed === 'string') {
try { parsed = JSON.parse(parsed) } catch { return [] }
}
const items = Array.isArray(parsed) ? parsed : (parsed && typeof parsed === 'object' ? [parsed] : [])
return items.reduce((pdfs, item, itemIndex) => {
const url = (item.file_url || item.url || '').toString().trim()
const name = (item.file_name || item.name || '').toString().trim()
const ext = String((name.includes('.') ? name.split('.').pop() : '') || url.split('?')[0].split('#')[0].split('.').pop() || '').toLowerCase()
if (ext !== 'pdf' || !url) return pdfs
const suffix = field.key || field.field_key || field.element_id || field.id || 'unknown'
const occurrenceKey = props.meetingMinutesContextPrefix
? `${props.meetingMinutesContextPrefix}:${suffix}:item:${itemIndex}`
: `planned-meeting-minutes:${suffix}:item:${itemIndex}`
if ((!props.resolveMeetingMinutesUsage || !props.resolveMeetingMinutesUsage(occurrenceKey, item).isDuplicate) && !pdfs.includes(url)) {
pdfs.push(url)
}
return pdfs
}, [])
}
const getMeetingMinutesUsageState = (field) => {
if (!field || field.element_type !== 'meeting_minutes') {
return { isDuplicate: false, title: null, fingerprint: null }
@ -462,6 +487,17 @@ const getMeetingMinutesUsageState = (field) => {
return props.resolveMeetingMinutesUsage(occurrenceKey, getFieldValue(field))
}
const getMeetingMinutesItemUsage = (field, item, itemIndex) => {
if (typeof props.resolveMeetingMinutesUsage !== 'function') {
return { isDuplicate: false, title: null, fingerprint: null }
}
const suffix = field.key || field.field_key || field.element_id || field.id || 'unknown'
const occurrenceKey = props.meetingMinutesContextPrefix
? `${props.meetingMinutesContextPrefix}:${suffix}:item:${itemIndex}`
: `planned-meeting-minutes:${suffix}:item:${itemIndex}`
return props.resolveMeetingMinutesUsage(occurrenceKey, item)
}
// 检查勾选清单是否选中
const isChecklistChecked = (value, optionValue) => {
if (!value) return false
@ -1755,7 +1791,3 @@ const isEmptyDetailTableValue = (val) => {

@ -293,7 +293,27 @@
<el-table-column prop="label" label="字段名称" width="200" />
<el-table-column prop="value" label="字段值">
<template #default="{ row }">
{{ formatFieldValue(row.value, row, selectionOptionsMap) }}
<template v-if="row.type === 'file'">
<template v-if="getFileItems(row.value).length">
<template v-for="(file, index) in getFileItems(row.value)" :key="`${file.url || file.name}_${index}`">
<el-link
v-if="file.url"
:href="file.url"
target="_blank"
type="primary"
:underline="false"
>
{{ file.name }}
</el-link>
<span v-else>{{ file.name }}</span>
<span v-if="index < getFileItems(row.value).length - 1">、</span>
</template>
</template>
<span v-else>-</span>
</template>
<template v-else>
{{ formatFieldValue(row.value, row, selectionOptionsMap) }}
</template>
</template>
</el-table-column>
<el-table-column prop="type" label="字段类型" width="120" />
@ -500,6 +520,33 @@ const parseChoiceValueToIds = (value) => {
.filter(v => v.length > 0)
}
// OA 动态表单中的 file 字段以 JSON 数组保存;兼容上传组件的 response 包装格式。
const getFileItems = (value) => {
if (value === null || value === undefined || value === '') return []
let parsed = value
for (let i = 0; i < 2 && typeof parsed === 'string'; i += 1) {
try {
parsed = JSON.parse(parsed)
} catch {
break
}
}
const values = Array.isArray(parsed) ? parsed : [parsed]
return values.map((item, index) => {
const file = item?.response && typeof item.response === 'object' ? item.response : item
if (typeof file === 'string') {
return { name: file.split('/').pop() || `附件${index + 1}`, url: file }
}
if (!file || typeof file !== 'object') return null
const url = file.url || file.file_url || file.path || file.file_path || null
const name = file.original_name || file.name || file.file_name || file.filename || (url ? String(url).split('/').pop() : `附件${index + 1}`)
return name || url ? { name, url } : null
}).filter(Boolean)
}
// 格式化字段值
const formatFieldValue = (value, col, selectionOptionsMap = {}) => {
if (value === null || value === undefined || value === '') {

@ -2474,7 +2474,7 @@ const loadTemplateElements = async () => {
const detailTableLoadPromises = []
templateElements.value.forEach(element => {
if (element.type === 'meeting_minutes') {
formData.fields[element.id] = null
formData.fields[element.id] = []
} else if (element.type === 'approval_flow' && !element.is_main_approval_process) {
// 非主要流程的审批流程字段初始化为空
formData.fields[element.id] = ''

@ -3531,8 +3531,8 @@ const initializeFormDataFromTemplate = (config) => {
// 文件字段初始化为空数组(支持多附件)
formData.value[field.key] = []
} else if (field.element_type === 'meeting_minutes') {
// 会议纪要类型初始化为 null
formData.value[field.key] = null
// 会议纪要统一为数组值
formData.value[field.key] = []
} else if (field.element_type === 'detail_table') {
// 明细表格初始化为空数组
formData.value[field.key] = []
@ -3569,8 +3569,8 @@ const initializeFormDataFromTemplate = (config) => {
// 文件字段初始化为空数组(支持多附件)
formData.value[field.key] = []
} else if (field.element_type === 'meeting_minutes') {
// 会议纪要类型初始化为 null
formData.value[field.key] = null
// 会议纪要统一为数组值
formData.value[field.key] = []
} else if (field.element_type === 'detail_table') {
// 明细表格初始化为空数组
formData.value[field.key] = []
@ -4319,20 +4319,13 @@ const formatPreviewFieldValue = (value, elementType, field) => {
// 如果是会议纪要字段
if (elementType === 'meeting_minutes') {
if (!value) {
const items = Array.isArray(value) ? value : (value && typeof value === 'object' ? [value] : [])
if (items.length === 0) {
return '未选择会议纪要'
}
// 如果值是对象,显示标题
if (typeof value === 'object' && value !== null) {
if (value.mode === 'oa_meeting_minutes' && value.title) {
return value.title
} else if (value.mode === 'upload' && value.name) {
return `附件: ${value.name}`
}
return '会议纪要'
}
// 如果是字符串,直接返回
return String(value)
return items.map(item => item.mode === 'upload'
? `附件: ${item.name || '附件'}`
: (item.title || '会议纪要')).join(';')
}
// 其他字段使用通用格式化
@ -5493,7 +5486,7 @@ const loadPaymentTemplateElements = async () => {
// 初始化表单字段
paymentTemplateElements.value.forEach(element => {
if (element.type === 'meeting_minutes') {
paymentFormData.fields[element.id] = null
paymentFormData.fields[element.id] = []
} else if (element.type === 'approval_flow' && !element.is_main_approval_process) {
paymentFormData.fields[element.id] = ''
} else if (element.type === 'checklist') {

@ -179,7 +179,8 @@
:model-value="payment?.fields?.[el.id]"
readonly
:title-only="getPaymentMeetingMinutesUsageState(el).isDuplicate"
:repeat-notice="getPaymentMeetingMinutesUsageState(el).isDuplicate ? REPEATED_MEETING_MINUTES_MESSAGE : ''"
:repeat-notice="REPEATED_MEETING_MINUTES_MESSAGE"
:item-usage-resolver="(item, index) => getPaymentMeetingMinutesItemUsage(el, item, index)"
/>
</div>
@ -261,16 +262,17 @@
</td>
</tr>
<!-- 会议纪要:若唯一附件为 PDF,则在本行后追加一个通栏行展示 PDF(尽量拉宽) -->
<!-- 会议纪要数组中的每个 PDF 单独输出。 -->
<tr
v-if="el.type === 'meeting_minutes' && !getPaymentMeetingMinutesUsageState(el).isDuplicate && getMeetingMinutesPdfUrl(payment?.fields?.[el.id])"
v-for="(pdf, itemIndex) in (el.type === 'meeting_minutes' ? getMeetingMinutesPdfItems(payment?.fields?.[el.id], el.id) : [])"
:key="`meeting-pdf-${el.id}-${itemIndex}`"
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])"
:data-pdf-id="`meeting_minutes_${el.id}_${itemIndex}`"
:data-pdf-url="pdf"
>
<div class="pdf-pages-container">
<div class="pdf-loading">PDF 加载中...</div>
@ -1667,6 +1669,11 @@ const getPaymentMeetingMinutesUsageState = (element) => {
return resolveMeetingMinutesUsage(occurrenceKey, payment.value?.fields?.[element?.id])
}
const getPaymentMeetingMinutesItemUsage = (element, item, itemIndex) => {
const occurrenceKey = `payment:${payment.value?.id || 'unknown'}:element:${element?.id || 'unknown'}:item:${itemIndex}`
return resolveMeetingMinutesUsage(occurrenceKey, item)
}
// 当 Payment 关联对象为 planned_expenditure 时,按 related_id 获取事前流程实体并加载对应模板
const loadRelatedPlannedExpenditure = async () => {
relatedPlannedExpenditure.value = null
@ -2002,6 +2009,18 @@ const getMeetingMinutesPdfUrl = (rawVal) => {
return ext === 'pdf' ? url : null
}
const getMeetingMinutesPdfItems = (rawVal, elementId) => {
const parsed = parseMeetingMinutesValue(rawVal)
const items = Array.isArray(parsed) ? parsed : (parsed && typeof parsed === 'object' ? [parsed] : [])
return items.reduce((pdfs, item, itemIndex) => {
const pdf = getMeetingMinutesPdfUrl(item)
if (!pdf) return pdfs
const occurrenceKey = `payment:${payment.value?.id || 'unknown'}:element:${elementId}:item:${itemIndex}`
if (!resolveMeetingMinutesUsage(occurrenceKey, item).isDuplicate && !pdfs.includes(pdf)) pdfs.push(pdf)
return pdfs
}, [])
}
// 关闭
const handleClose = () => {
// 新窗口打开时可关闭;若用户直接访问打印页,则回退

Loading…
Cancel
Save