会议纪要重复,在查看和打印页面按照同一个会议纪要只渲染一次的方式进行去重,重复的会议纪要仅保留名称_1

master
weizong song 1 month ago
parent 889c343f65
commit 5dc8db772b

@ -98,8 +98,13 @@
<!-- 展示态 -->
<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-if="displayValue && displayValue.mode === 'oa_meeting_minutes'" class="readonly-item readonly-oa">
<div v-else-if="displayValue && displayValue.mode === 'oa_meeting_minutes'" class="readonly-item readonly-oa">
<el-link
type="primary"
:underline="false"
@ -501,6 +506,14 @@ const props = defineProps({
placeholder: {
type: String,
default: '请选择会议纪要或上传附件'
},
titleOnly: {
type: Boolean,
default: false
},
repeatNotice: {
type: String,
default: ''
}
})
@ -562,6 +575,11 @@ const displayValue = computed(() => {
return null
})
const duplicateTitle = computed(() => {
if (!displayValue.value || typeof displayValue.value !== 'object') return '会议纪要'
return displayValue.value.title || displayValue.value.name || '会议纪要'
})
// UI data
const fetchMeetingMinuteDetail = async (id) => {
try {
@ -1058,6 +1076,13 @@ watch(showSelectDialog, (visible) => {
border-color: transparent;
}
.readonly-duplicate {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.readonly-title {
flex: 0 1 auto;
font-size: 14px;
@ -1073,6 +1098,12 @@ watch(showSelectDialog, (visible) => {
color: inherit;
}
.readonly-repeat-note {
font-size: 12px;
line-height: 1.5;
color: #b45309;
}
.readonly-actions {
flex-shrink: 0;
display: flex;
@ -1437,4 +1468,3 @@ watch(showSelectDialog, (visible) => {
}
</style>

@ -77,6 +77,8 @@
<MeetingMinutesField
:model-value="getFieldValue(field)"
readonly
:title-only="getMeetingMinutesUsageState(field).isDuplicate"
:repeat-notice="getMeetingMinutesUsageState(field).isDuplicate ? REPEATED_MEETING_MINUTES_MESSAGE : ''"
/>
</div>
<!-- 明细表格类型卡片渲染 -->
@ -201,7 +203,7 @@
<!-- 会议纪要若唯一附件为 PDF则在本行后追加一个通栏行展示 PDF尽量拉宽 -->
<tr
v-if="field.element_type === 'meeting_minutes' && getMeetingMinutesPdfUrl(field)"
v-if="field.element_type === 'meeting_minutes' && !getMeetingMinutesUsageState(field).isDuplicate && getMeetingMinutesPdfUrl(field)"
class="meeting-minutes-pdf-row"
>
<td class="value meeting-minutes-pdf-td" colspan="2">
@ -316,10 +318,19 @@ const props = defineProps({
hideEmpty: {
type: Boolean,
default: false
},
meetingMinutesContextPrefix: {
type: String,
default: ''
},
resolveMeetingMinutesUsage: {
type: Function,
default: null
}
})
const emit = defineEmits(['collect-flow-id'])
const REPEATED_MEETING_MINUTES_MESSAGE = '会议纪要再次使用,仅展示标题,不重复打印'
/**
* 兼容两种 elementValues 结构
@ -435,6 +446,22 @@ const getMeetingMinutesPdfUrl = (field) => {
return ext === 'pdf' ? url : null
}
const getMeetingMinutesUsageState = (field) => {
if (!field || field.element_type !== 'meeting_minutes') {
return { isDuplicate: false, title: null, fingerprint: null }
}
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}`
: `planned-meeting-minutes:${suffix}`
return props.resolveMeetingMinutesUsage(occurrenceKey, getFieldValue(field))
}
//
const isChecklistChecked = (value, optionValue) => {
if (!value) return false
@ -1732,5 +1759,3 @@ const isEmptyDetailTableValue = (val) => {

@ -92,6 +92,8 @@
:element-values="exp.element_values || {}"
:flow-bindings="exp.flow_bindings || []"
:hide-empty="true"
:meeting-minutes-context-prefix="`${resolvedMeetingMinutesContextPrefix}:exp:${exp.id}`"
:resolve-meeting-minutes-usage="resolveMeetingMinutesUsage"
@collect-flow-id="handleCollectFlowId"
/>
</div>
@ -109,7 +111,9 @@ import PlannedExpenditureTemplateReadonly from '@/components/PlannedExpenditureT
const props = defineProps({
contractId: { type: [Number, String], default: null },
currentPaymentId: { type: [Number, String], default: null },
embedPlannedExpenditures: { type: Boolean, default: false }
embedPlannedExpenditures: { type: Boolean, default: false },
meetingMinutesContextPrefix: { type: String, default: '' },
resolveMeetingMinutesUsage: { type: Function, default: null }
})
const emit = defineEmits(['collect-flow-id'])
@ -170,6 +174,11 @@ const getTemplateForExpenditure = (exp) => {
return templatesCache.value[exp.category_id] || null
}
const resolvedMeetingMinutesContextPrefix = computed(() => {
if (props.meetingMinutesContextPrefix) return props.meetingMinutesContextPrefix
return `contract:${props.contractId || 'unknown'}`
})
const reload = async () => {
const cid = props.contractId ? Number(props.contractId) : null
if (!cid) {
@ -363,4 +372,3 @@ watch(
}
</style>

@ -17,6 +17,8 @@
:element-values="expenditure.element_values || {}"
:flow-bindings="expenditure.flow_bindings || []"
:hide-empty="true"
:meeting-minutes-context-prefix="resolvedMeetingMinutesContextPrefix"
:resolve-meeting-minutes-usage="resolveMeetingMinutesUsage"
@collect-flow-id="handleCollectFlowId"
/>
</div>
@ -30,7 +32,9 @@ import { plannedExpenditureCategoryAPI } from '@/utils/api'
const props = defineProps({
expenditure: { type: Object, required: true },
template: { type: Object, default: null }
template: { type: Object, default: null },
meetingMinutesContextPrefix: { type: String, default: '' },
resolveMeetingMinutesUsage: { type: Function, default: null }
})
const emit = defineEmits(['collect-flow-id'])
@ -94,6 +98,11 @@ const breadcrumbItems = computed(() => {
if (props.template?.name) return [{ id: null, name: props.template.name }]
return []
})
const resolvedMeetingMinutesContextPrefix = computed(() => {
if (props.meetingMinutesContextPrefix) return props.meetingMinutesContextPrefix
return `planned-expenditure:${props.expenditure?.id || 'unknown'}`
})
</script>
<style scoped>
@ -132,4 +141,3 @@ const breadcrumbItems = computed(() => {
}
</style>

@ -104,6 +104,8 @@
<MeetingMinutesField
:model-value="payment?.fields?.[el.id]"
readonly
:title-only="getPaymentMeetingMinutesUsageState(el).isDuplicate"
:repeat-notice="getPaymentMeetingMinutesUsageState(el).isDuplicate ? REPEATED_MEETING_MINUTES_MESSAGE : ''"
/>
</div>
@ -187,7 +189,7 @@
<!-- 会议纪要若唯一附件为 PDF则在本行后追加一个通栏行展示 PDF尽量拉宽 -->
<tr
v-if="el.type === 'meeting_minutes' && getMeetingMinutesPdfUrl(payment?.fields?.[el.id])"
v-if="el.type === 'meeting_minutes' && !getPaymentMeetingMinutesUsageState(el).isDuplicate && getMeetingMinutesPdfUrl(payment?.fields?.[el.id])"
class="meeting-minutes-pdf-row"
>
<td class="value meeting-minutes-pdf-td" colspan="4">
@ -214,6 +216,8 @@
:contract-id="payment.contract_id"
:current-payment-id="payment?.id"
:embed-planned-expenditures="true"
:meeting-minutes-context-prefix="`print:contract:${payment.contract_id}`"
:resolve-meeting-minutes-usage="resolveMeetingMinutesUsage"
@collect-flow-id="collectFlowId"
/>
</template>
@ -223,6 +227,8 @@
:contract-id="payment?.related_id"
:current-payment-id="payment?.id"
:embed-planned-expenditures="true"
:meeting-minutes-context-prefix="`print:related-contract:${payment?.related_id}`"
:resolve-meeting-minutes-usage="resolveMeetingMinutesUsage"
@collect-flow-id="collectFlowId"
/>
</template>
@ -232,6 +238,8 @@
v-if="relatedPlannedExpenditure"
:expenditure="relatedPlannedExpenditure"
:template="relatedPlannedExpenditureTemplate"
:meeting-minutes-context-prefix="`print:planned-expenditure:${relatedPlannedExpenditure?.id || payment?.related_id}`"
:resolve-meeting-minutes-usage="resolveMeetingMinutesUsage"
@collect-flow-id="collectFlowId"
/>
@ -241,6 +249,8 @@
:contract-id="payment.contract_id"
:current-payment-id="payment?.id"
:embed-planned-expenditures="false"
:meeting-minutes-context-prefix="`print:contract:${payment.contract_id}`"
:resolve-meeting-minutes-usage="resolveMeetingMinutesUsage"
@collect-flow-id="collectFlowId"
/>
</template>
@ -364,6 +374,97 @@ let flowIframeLoadTimeout = null
// ID
const collectedFlowIds = ref(new Set())
const renderedPrintTemplates = ref([]) // HTML
const REPEATED_MEETING_MINUTES_MESSAGE = '会议纪要再次使用,仅展示标题,不重复打印'
let meetingMinutesUsageStack = []
let meetingMinutesUsageCache = {}
let meetingMinutesSeenMap = new Map()
const resetMeetingMinutesUsage = () => {
meetingMinutesUsageStack = []
meetingMinutesUsageCache = {}
meetingMinutesSeenMap = new Map()
}
const parseMeetingMinutesValue = (rawVal) => {
if (rawVal === null || rawVal === undefined || rawVal === '') return null
if (typeof rawVal === 'string') {
try {
const parsed = JSON.parse(rawVal)
if (parsed && typeof parsed === 'object') return parsed
} catch {
// ignore
}
return rawVal
}
return rawVal
}
const getMeetingMinutesDisplayTitle = (rawVal) => {
const parsed = parseMeetingMinutesValue(rawVal)
if (parsed && typeof parsed === 'object') {
return parsed.title || parsed.name || parsed.display_name || '会议纪要'
}
if (typeof parsed === 'string' && parsed.trim()) {
return parsed.trim()
}
return '会议纪要'
}
const extractMeetingMinuteId = (rawVal) => {
const parsed = parseMeetingMinutesValue(rawVal)
if (!parsed || typeof parsed !== 'object') return null
const id = parsed.meeting_minute_id
if (id === null || id === undefined || id === '') return null
const normalized = Number(id)
if (!Number.isFinite(normalized) || normalized <= 0) return null
return normalized
}
const buildMeetingMinutesFingerprint = (rawVal) => {
const meetingMinuteId = extractMeetingMinuteId(rawVal)
if (meetingMinuteId) {
return `meeting_minute_id:${meetingMinuteId}`
}
return null
}
const resolveMeetingMinutesUsage = (occurrenceKey, rawVal) => {
if (!occurrenceKey) {
return { isDuplicate: false, title: getMeetingMinutesDisplayTitle(rawVal), fingerprint: null }
}
if (meetingMinutesUsageCache[occurrenceKey]) {
return meetingMinutesUsageCache[occurrenceKey]
}
const fingerprint = buildMeetingMinutesFingerprint(rawVal)
if (!fingerprint) {
const emptyState = { isDuplicate: false, title: getMeetingMinutesDisplayTitle(rawVal), fingerprint: null }
meetingMinutesUsageCache[occurrenceKey] = emptyState
return emptyState
}
const firstOccurrenceKey = meetingMinutesSeenMap.get(fingerprint) || null
const state = {
isDuplicate: !!firstOccurrenceKey,
title: getMeetingMinutesDisplayTitle(rawVal),
fingerprint,
firstOccurrenceKey
}
if (!firstOccurrenceKey) {
meetingMinutesSeenMap.set(fingerprint, occurrenceKey)
}
meetingMinutesUsageStack.push({
occurrenceKey,
fingerprint,
title: state.title,
isDuplicate: state.isDuplicate
})
meetingMinutesUsageCache[occurrenceKey] = state
return state
}
// ID
const collectFlowId = (flowId) => {
@ -645,6 +746,11 @@ const visiblePaymentTemplateElements = computed(() => {
})
})
const getPaymentMeetingMinutesUsageState = (element) => {
const occurrenceKey = `payment:${payment.value?.id || 'unknown'}:element:${element?.id || 'unknown'}`
return resolveMeetingMinutesUsage(occurrenceKey, payment.value?.fields?.[element?.id])
}
// Payment planned_expenditure related_id
const loadRelatedPlannedExpenditure = async () => {
relatedPlannedExpenditure.value = null
@ -679,6 +785,7 @@ const loadPaymentDetail = async () => {
}
loading.value = true
resetMeetingMinutesUsage()
// ID
collectedFlowIds.value.clear()
@ -1803,5 +1910,3 @@ onMounted(async () => {
font-size: 14px;
}
</style>

Loading…
Cancel
Save