+
{
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;
}
-
-
|