From 5dc8db772bfbc480b50f6d908516901b22a90548 Mon Sep 17 00:00:00 2001 From: weizong song Date: Mon, 16 Mar 2026 01:47:16 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=9A=E8=AE=AE=E7=BA=AA=E8=A6=81=E9=87=8D?= =?UTF-8?q?=E5=A4=8D,=E5=9C=A8=E6=9F=A5=E7=9C=8B=E5=92=8C=E6=89=93?= =?UTF-8?q?=E5=8D=B0=E9=A1=B5=E9=9D=A2=E6=8C=89=E7=85=A7=E5=90=8C=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E4=BC=9A=E8=AE=AE=E7=BA=AA=E8=A6=81=E5=8F=AA=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E4=B8=80=E6=AC=A1=E7=9A=84=E6=96=B9=E5=BC=8F=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E5=8E=BB=E9=87=8D,=E9=87=8D=E5=A4=8D=E7=9A=84?= =?UTF-8?q?=E4=BC=9A=E8=AE=AE=E7=BA=AA=E8=A6=81=E4=BB=85=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E5=90=8D=E7=A7=B0=5F1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/MeetingMinutesField.vue | 34 +++++- .../PlannedExpenditureTemplateReadonly.vue | 31 ++++- .../payment-print/ContractInfoCard.vue | 12 +- .../PlannedExpenditureInfoCard.vue | 12 +- src/views/payment/PaymentDetailPrint.vue | 111 +++++++++++++++++- 5 files changed, 188 insertions(+), 12 deletions(-) diff --git a/src/components/MeetingMinutesField.vue b/src/components/MeetingMinutesField.vue index 7439514..badd787 100644 --- a/src/components/MeetingMinutesField.vue +++ b/src/components/MeetingMinutesField.vue @@ -98,8 +98,13 @@
+
+ {{ duplicateTitle }} + {{ repeatNotice }} +
+ -
+
{ 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) => { } - diff --git a/src/components/PlannedExpenditureTemplateReadonly.vue b/src/components/PlannedExpenditureTemplateReadonly.vue index f3c4a0c..b112157 100644 --- a/src/components/PlannedExpenditureTemplateReadonly.vue +++ b/src/components/PlannedExpenditureTemplateReadonly.vue @@ -77,6 +77,8 @@
@@ -201,7 +203,7 @@ @@ -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) => { - - diff --git a/src/components/payment-print/ContractInfoCard.vue b/src/components/payment-print/ContractInfoCard.vue index 498ff02..d1e1288 100644 --- a/src/components/payment-print/ContractInfoCard.vue +++ b/src/components/payment-print/ContractInfoCard.vue @@ -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" />
@@ -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( } - diff --git a/src/components/payment-print/PlannedExpenditureInfoCard.vue b/src/components/payment-print/PlannedExpenditureInfoCard.vue index 95a72a5..04ace88 100644 --- a/src/components/payment-print/PlannedExpenditureInfoCard.vue +++ b/src/components/payment-print/PlannedExpenditureInfoCard.vue @@ -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" />
@@ -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'}` +}) - diff --git a/src/views/payment/PaymentDetailPrint.vue b/src/views/payment/PaymentDetailPrint.vue index e995ca6..3ef44ad 100644 --- a/src/views/payment/PaymentDetailPrint.vue +++ b/src/views/payment/PaymentDetailPrint.vue @@ -104,6 +104,8 @@ @@ -187,7 +189,7 @@ @@ -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" /> @@ -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" /> @@ -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" /> @@ -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; } - -