+
+
-
执行率
-
- {{ offsetPrepaidExecutionRate }}%
-
+
工会
+
¥{{ formatNumber(summary.tradeUnionBudget) }}
+
+
+
已使用
+
¥{{ formatNumber(summary.tradeUnionUsed) }}
+
+
+
待使用
+
¥{{ formatNumber(summary.tradeUnionBudget - summary.tradeUnionUsed) }}
@@ -539,11 +553,13 @@ const summary = ref({
lastYearCarryoverBudget: 0, // 上一年结转资金
projectBudget: 0, // 自有账户
offsetPrepaidBudget: 0, // 抵消预付账款
+ tradeUnionBudget: 0, // 工会
departmentExecuted: 0,
specialFundUsed: 0,
lastYearCarryoverUsed: 0,
projectUsed: 0,
- offsetPrepaidUsed: 0
+ offsetPrepaidUsed: 0,
+ tradeUnionUsed: 0
})
const budgetTree = ref([])
const loading = ref(false)
@@ -597,6 +613,12 @@ const offsetPrepaidExecutionRate = computed(() => {
return ((summary.value.offsetPrepaidUsed / summary.value.offsetPrepaidBudget) * 100).toFixed(2)
})
+// 计算工会执行率
+const tradeUnionExecutionRate = computed(() => {
+ if (summary.value.tradeUnionBudget === 0) return '0.00'
+ return ((summary.value.tradeUnionUsed / summary.value.tradeUnionBudget) * 100).toFixed(2)
+})
+
// 根据执行率返回颜色
const getSummaryRateColor = (rate) => {
const numRate = Number(rate)
@@ -747,7 +769,8 @@ const budgetTypeLabel = (type) => {
project: '自有账户',
special_fund: '专项资金',
last_year_carryover: '上一年结转资金',
- offset_prepaid: '抵消预付账款'
+ offset_prepaid: '抵消预付账款',
+ trade_union: '工会'
}
return map[type] || '未指定'
}
@@ -771,13 +794,17 @@ const lastYearCarryoverBudget = computed(() =>
const offsetPrepaidBudget = computed(() =>
(budgetTree.value || []).filter(item => (item.budget_type || 'department') === 'offset_prepaid')
)
+const tradeUnionBudget = computed(() =>
+ (budgetTree.value || []).filter(item => (item.budget_type || 'department') === 'trade_union')
+)
const budgetSections = computed(() => [
{ key: 'department', label: '部门预算', data: departmentBudget.value, tagType: 'success' },
{ key: 'special_fund', label: '专项资金', data: specialFundBudget.value, tagType: 'warning' },
{ key: 'last_year_carryover', label: '上一年结转资金', data: lastYearCarryoverBudget.value, tagType: 'danger' },
{ key: 'project', label: '自有账户', data: projectBudget.value, tagType: 'info' },
- { key: 'offset_prepaid', label: '抵消预付账款', data: offsetPrepaidBudget.value, tagType: 'primary' }
+ { key: 'offset_prepaid', label: '抵消预付账款', data: offsetPrepaidBudget.value, tagType: 'primary' },
+ { key: 'trade_union', label: '工会', data: tradeUnionBudget.value, tagType: 'info' }
])
const expandAll = (typeKey = null) => {
@@ -793,6 +820,8 @@ const expandAll = (typeKey = null) => {
list = lastYearCarryoverBudget.value
} else if (typeKey === 'offset_prepaid') {
list = offsetPrepaidBudget.value
+ } else if (typeKey === 'trade_union') {
+ list = tradeUnionBudget.value
} else {
list = budgetTree.value
}
@@ -817,6 +846,8 @@ const collapseAll = (typeKey = null) => {
list = lastYearCarryoverBudget.value
} else if (typeKey === 'offset_prepaid') {
list = offsetPrepaidBudget.value
+ } else if (typeKey === 'trade_union') {
+ list = tradeUnionBudget.value
} else {
list = budgetTree.value
}
@@ -928,12 +959,16 @@ const updateSummary = async () => {
// 计算抵消预付账款(budget_type === 'offset_prepaid' 的第一层节点)
const offsetPrepaidTotal = offsetPrepaidBudget.value.reduce((sum, n) => sum + Number(n.budget_amount || 0), 0)
+ // 计算工会(budget_type === 'trade_union' 的第一层节点)
+ const tradeUnionTotal = tradeUnionBudget.value.reduce((sum, n) => sum + Number(n.budget_amount || 0), 0)
+
summary.value.totalBudget = total
summary.value.departmentBudget = departmentTotal
summary.value.specialFundBudget = specialFundTotal
summary.value.lastYearCarryoverBudget = lastYearCarryoverTotal
summary.value.projectBudget = projectTotal
summary.value.offsetPrepaidBudget = offsetPrepaidTotal
+ summary.value.tradeUnionBudget = tradeUnionTotal
// 获取执行统计
await loadExecutionStatistics()
@@ -955,6 +990,7 @@ const loadExecutionStatistics = async () => {
summary.value.lastYearCarryoverUsed = res.data.last_year_carryover_used || 0
summary.value.projectUsed = res.data.project_used || 0
summary.value.offsetPrepaidUsed = res.data.offset_prepaid_used || 0
+ summary.value.tradeUnionUsed = res.data.trade_union_used || 0
}
} catch (e) {
console.error('加载执行统计失败:', e)
@@ -1167,6 +1203,40 @@ onMounted(async () => {
font-weight: 700;
}
+/* 工会通栏 - 青色渐变风格 */
+.summary-row-card-trade-union {
+ background: linear-gradient(135deg, #0d9488 0%, #99f6e4 100%);
+ border: none;
+ box-shadow: 0 4px 12px rgba(13, 148, 136, 0.2);
+}
+
+.summary-row-card-trade-union :deep(.el-card__body) {
+ padding: 24px 20px;
+}
+
+.summary-row-card-trade-union .summary-row-label {
+ color: #0f766e;
+ font-size: 15px;
+ font-weight: 500;
+}
+
+.summary-row-card-trade-union .summary-row-value {
+ color: #115e59;
+ font-size: 24px;
+ font-weight: 700;
+}
+
+/* 抵消预付账款明细项表头 - 紫色渐变风格(调淡) */
+.budget-level-1-header-trade_union {
+ background: linear-gradient(135deg, #0d9488 0%, #99f6e4 100%) !important;
+ color: #115e59 !important;
+}
+
+.budget-level-1-header-trade_union.collapsed {
+ background: #99f6e4 !important;
+ color: #115e59 !important;
+}
+
.summary-row {
display: flex;
align-items: center;
@@ -1334,6 +1404,11 @@ onMounted(async () => {
border-color: #8b5cf6 !important;
}
+.type-section-card-trade_union :deep(.type-title .el-tag--dark) {
+ background-color: #0d9488 !important;
+ border-color: #0d9488 !important;
+}
+
.type-header {
display: flex;
justify-content: space-between;
diff --git a/src/views/funds/BudgetManagement.vue b/src/views/funds/BudgetManagement.vue
index 2519ce1..fad744f 100644
--- a/src/views/funds/BudgetManagement.vue
+++ b/src/views/funds/BudgetManagement.vue
@@ -330,10 +330,11 @@ import { budgetDataAPI } from '@/utils/api'
const budgetTypeOptions = [
{ value: 'department', label: '部门预算' },
- { value: 'project', label: '自有账户' },
{ value: 'special_fund', label: '专项资金' },
{ value: 'last_year_carryover', label: '上一年结转资金' },
- { value: 'offset_prepaid', label: '抵消预付账款' }
+ { value: 'offset_prepaid', label: '抵消预付账款' },
+ { value: 'project', label: '自有账户' },
+ { value: 'trade_union', label: '工会' }
]
const loading = ref(false)
@@ -418,6 +419,7 @@ const formatNumber = (num) => {
const getBudgetTypeLabel = (type) => {
const map = {
department: '部门预算',
+ trade_union: '工会',
project: '自有账户',
special_fund: '专项资金',
last_year_carryover: '上一年结转资金',
@@ -441,13 +443,17 @@ const lastYearCarryoverTree = computed(() =>
const offsetPrepaidTree = computed(() =>
(treeData.value || []).filter(item => (item.budget_type || 'department') === 'offset_prepaid')
)
+const tradeUnionTree = computed(() =>
+ (treeData.value || []).filter(item => (item.budget_type || 'department') === 'trade_union')
+)
const typeSections = computed(() => [
{ key: 'department', label: '部门预算', data: departmentTree.value, tagType: 'success' },
{ key: 'special_fund', label: '专项资金', data: specialFundTree.value, tagType: 'warning' },
{ key: 'last_year_carryover', label: '上一年结转资金', data: lastYearCarryoverTree.value, tagType: 'danger' },
{ key: 'offset_prepaid', label: '抵消预付账款', data: offsetPrepaidTree.value, tagType: 'primary' },
- { key: 'project', label: '自有账户', data: projectTree.value, tagType: 'info' }
+ { key: 'project', label: '自有账户', data: projectTree.value, tagType: 'info' },
+ { key: 'trade_union', label: '工会', data: tradeUnionTree.value, tagType: 'info' }
])
const setTreeRef = (key, el) => {
@@ -699,9 +705,25 @@ const handleSubmit = async () => {
submitting.value = true
try {
const isEdit = !!formData.id
+ let payload
+ if (isEdit) {
+ payload = formData
+ } else {
+ // 新增时显式传参,根节点必须带 budget_type,避免丢失「工会」等类型
+ payload = {
+ year_id: formData.year_id,
+ parent_id: formData.parent_id ?? null,
+ name: formData.name,
+ code: formData.code ?? '',
+ budget_amount: formData.budget_amount,
+ description: formData.description ?? '',
+ sort_order: formData.sort_order ?? 0,
+ budget_type: formData.parent_id ? (formData.budget_type || budgetTypeOptions[0].value) : (formData.budget_type || budgetTypeOptions[0].value)
+ }
+ }
const response = isEdit
- ? await budgetDataAPI.update(formData.id, formData)
- : await budgetDataAPI.create(formData)
+ ? await budgetDataAPI.update(formData.id, payload)
+ : await budgetDataAPI.create(payload)
if (response.code === 0) {
ElMessage.success(isEdit ? '更新成功' : '创建成功')
diff --git a/src/views/payment/IndirectPayment.vue b/src/views/payment/IndirectPayment.vue
index b6d210e..ca8ca9d 100644
--- a/src/views/payment/IndirectPayment.vue
+++ b/src/views/payment/IndirectPayment.vue
@@ -278,7 +278,8 @@
:on-error="(err) => handleFileUploadError(field.key, err)"
:on-remove="(file) => handleFileRemove(field.key, file)"
:before-upload="beforeUpload"
- :limit="1"
+ multiple
+ :limit="10"
class="file-upload-component"
>
@@ -287,7 +288,7 @@
- 支持上传单个文件,文件大小不超过10MB
+ 支持上传多个文件,单个文件大小不超过10MB
@@ -3419,8 +3420,8 @@ const initializeFormDataFromTemplate = (config) => {
})
}
} else if (field.element_type === 'file') {
- // 文件字段初始化为空字符串
- formData.value[field.key] = ''
+ // 文件字段初始化为空数组(支持多附件)
+ formData.value[field.key] = []
} else if (field.element_type === 'meeting_minutes') {
// 会议纪要类型初始化为 null
formData.value[field.key] = null
@@ -3457,8 +3458,8 @@ const initializeFormDataFromTemplate = (config) => {
})
}
} else if (field.element_type === 'file') {
- // 文件字段初始化为空字符串
- formData.value[field.key] = ''
+ // 文件字段初始化为空数组(支持多附件)
+ formData.value[field.key] = []
} else if (field.element_type === 'meeting_minutes') {
// 会议纪要类型初始化为 null
formData.value[field.key] = null
@@ -3624,56 +3625,89 @@ const normalizeChecklistData = () => {
})
}
-// 获取文件列表(用于 el-upload 组件显示)
-const getFileList = (fieldKey) => {
- const value = formData.value[fieldKey]
+// 将各种历史格式统一为附件列表(用于编辑、回显、提交)
+const normalizeFileUploadValue = (value) => {
if (!value) return []
-
- // 如果已经是数组格式(el-upload 需要的格式)
+
+ const normalizeItem = (raw, idx = 0) => {
+ if (!raw) return null
+
+ if (typeof raw === 'string') {
+ const s = raw.trim()
+ if (!s) return null
+ const fileName = s.split('/').pop() || `附件${idx + 1}`
+ return {
+ uid: `legacy-${Date.now()}-${idx}`,
+ name: fileName,
+ url: s
+ }
+ }
+
+ if (typeof raw === 'object') {
+ const id = raw.id ?? raw.uid ?? `legacy-${Date.now()}-${idx}`
+ const name = raw.original_name || raw.file_name || raw.name || `附件${idx + 1}`
+ const filePath = raw.file_path || (raw.folder && raw.name ? `${raw.folder}/${raw.name}` : '')
+ const url = raw.url || raw.path || raw.file_url || filePath || ''
+ return {
+ uid: id,
+ name,
+ url,
+ response: raw
+ }
+ }
+
+ return null
+ }
+
if (Array.isArray(value)) {
- return value
+ return value.map((item, idx) => normalizeItem(item, idx)).filter(Boolean)
}
-
- // 如果是字符串,尝试解析为 JSON
+
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value)
- // 如果是对象,转换为 el-upload 需要的格式
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
- return [{
- name: parsed.original_name || parsed.name || '文件',
- url: parsed.url || (parsed.folder && parsed.name ? `${parsed.folder}/${parsed.name}` : ''),
- uid: parsed.id || Date.now(),
- response: parsed // 保存完整的后端数据
- }]
- }
- // 如果解析后是数组,直接返回
if (Array.isArray(parsed)) {
- return parsed
+ return parsed.map((item, idx) => normalizeItem(item, idx)).filter(Boolean)
}
- } catch (e) {
- // 解析失败,可能是文件路径字符串
- // 尝试从路径中提取文件名
- const fileName = value.split('/').pop() || value
- return [{
- name: fileName,
- url: value,
- uid: Date.now()
- }]
+ const one = normalizeItem(parsed, 0)
+ return one ? [one] : []
+ } catch (_) {
+ const one = normalizeItem(value, 0)
+ return one ? [one] : []
}
}
-
- // 如果是对象,转换为 el-upload 需要的格式
- if (value && typeof value === 'object') {
- return [{
- name: value.original_name || value.name || '文件',
- url: value.url || (value.folder && value.name ? `${value.folder}/${value.name}` : ''),
- uid: value.id || Date.now(),
- response: value // 保存完整的后端数据
- }]
+
+ const one = normalizeItem(value, 0)
+ return one ? [one] : []
+}
+
+// 规范化模板中 file 字段,确保 formData 始终是附件列表
+const normalizeFileUploadData = () => {
+ if (!templateConfig.value) return
+
+ const normalizeField = (field) => {
+ if (field?.element_type !== 'file' || !field.key) return
+ formData.value[field.key] = normalizeFileUploadValue(formData.value[field.key])
}
-
- return []
+
+ Object.keys(templateConfig.value).forEach(sectionKey => {
+ const section = templateConfig.value[sectionKey]
+ if (section.fields && Array.isArray(section.fields)) {
+ section.fields.forEach(normalizeField)
+ }
+ if (section.rounds && Array.isArray(section.rounds)) {
+ section.rounds.forEach(round => {
+ if (round.fields && Array.isArray(round.fields)) {
+ round.fields.forEach(normalizeField)
+ }
+ })
+ }
+ })
+}
+
+// 获取文件列表(用于 el-upload 组件显示)
+const getFileList = (fieldKey) => {
+ return normalizeFileUploadValue(formData.value[fieldKey])
}
// 文件上传成功处理
@@ -3698,8 +3732,22 @@ const handleFileUploadSuccess = (fieldKey, response) => {
file_path: fileData.folder && fileData.name ? `${fileData.folder}/${fileData.name}` : null
}
- // 保存为 JSON 字符串(便于存储和传输)
- formData.value[fieldKey] = JSON.stringify(fileInfo)
+ // 统一存储为数组,便于多附件回显、二次编辑和后续展示
+ const currentFiles = normalizeFileUploadValue(formData.value[fieldKey])
+ const exists = currentFiles.some(item => {
+ const idA = item?.response?.id ?? item?.uid
+ const idB = fileInfo.id
+ return idA && idB && String(idA) === String(idB)
+ })
+ if (!exists) {
+ currentFiles.push({
+ uid: fileInfo.id || `upload-${Date.now()}`,
+ name: fileInfo.original_name || fileInfo.name || '文件',
+ url: fileInfo.file_path || '',
+ response: fileInfo
+ })
+ }
+ formData.value[fieldKey] = currentFiles
ElMessage.success('上传成功')
} else {
@@ -3712,8 +3760,15 @@ const handleFileUploadSuccess = (fieldKey, response) => {
// 文件移除处理
const handleFileRemove = (fieldKey, file) => {
- // 清空该字段的值
- formData.value[fieldKey] = ''
+ const currentFiles = normalizeFileUploadValue(formData.value[fieldKey])
+ const removedUid = file?.uid
+ const removedName = file?.name
+ const nextFiles = currentFiles.filter(item => {
+ if (removedUid && String(item.uid) === String(removedUid)) return false
+ if (removedName && item.name === removedName && item.url === file?.url) return false
+ return true
+ })
+ formData.value[fieldKey] = nextFiles
ElMessage.success('已移除文件')
}
@@ -3935,18 +3990,9 @@ const formatFieldValue = (value, elementType) => {
return value
}
if (elementType === 'file') {
- // 文件字段:尝试解析 JSON 字符串获取文件名
- try {
- const fileInfo = typeof value === 'string' ? JSON.parse(value) : value
- if (fileInfo && typeof fileInfo === 'object') {
- return fileInfo.original_name || fileInfo.name || '文件'
- }
- } catch (e) {
- // 解析失败,可能是文件路径字符串
- const fileName = value.split('/').pop() || value
- return fileName
- }
- return value
+ const files = normalizeFileUploadValue(value)
+ if (files.length === 0) return '-'
+ return files.map(file => file.name || '文件').join(',')
}
return value
}
@@ -3964,16 +4010,7 @@ const formatFieldValueByType = (value, type) => {
case 'number':
return typeof value === 'number' ? value.toLocaleString('zh-CN') : value
case 'file':
- // 如果是 JSON 字符串,解析后显示文件名
- try {
- const files = JSON.parse(value)
- if (Array.isArray(files)) {
- return files.map(f => f.name || f).join(', ')
- }
- } catch (e) {
- return value
- }
- return value
+ return normalizeFileUploadValue(value).map(f => f.name || '文件').join(', ') || '-'
default:
return value
}
@@ -6738,9 +6775,10 @@ const loadDraftData = async (id) => {
}
}
})
- // 规范化勾选清单数据格式(确保数组格式正确,补充缺失的备注字段)
+ // 规范化勾选清单与文件上传数据(兼容旧值,确保可二次编辑)
await nextTick()
normalizeChecklistData()
+ normalizeFileUploadData()
// related_contract 已移除,无需恢复合同字段
}
diff --git a/src/views/payment/PaymentDetailPrint.vue b/src/views/payment/PaymentDetailPrint.vue
index 2b67c70..e995ca6 100644
--- a/src/views/payment/PaymentDetailPrint.vue
+++ b/src/views/payment/PaymentDetailPrint.vue
@@ -1319,6 +1319,56 @@ onMounted(async () => {
height: auto;
}
+/* 相关流程详情内 relation 类型:每行数据做成卡片,参照本页「关联合同的事前流程」related-expenditure-block */
+.flow-template-content :deep(.relation-cards) {
+ width: 100%;
+}
+
+.flow-template-content :deep(.relation-row-card) {
+ border: 1px solid #e5e7eb;
+ border-radius: 10px;
+ padding: 14px 14px 10px;
+ margin-top: 12px;
+}
+
+.flow-template-content :deep(.relation-row-card:first-child) {
+ margin-top: 0;
+}
+
+.flow-template-content :deep(.relation-row-card .relation-card-table) {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 0;
+ border: none;
+}
+
+.flow-template-content :deep(.relation-row-card .relation-card-table tr) {
+ border-bottom: 1px solid #f0f2f4;
+}
+
+.flow-template-content :deep(.relation-row-card .relation-card-table tr:last-child) {
+ border-bottom: none;
+}
+
+.flow-template-content :deep(.relation-row-card .relation-card-table td) {
+ padding: 8px 12px;
+ vertical-align: top;
+ word-break: break-word;
+ border: none;
+ line-height: 1.4;
+}
+
+.flow-template-content :deep(.relation-row-card .relation-card-table .label) {
+ width: 120px;
+ font-weight: 700;
+ color: #111827;
+ background: #f9fafb;
+}
+
+.flow-template-content :deep(.relation-row-card .relation-card-table .value) {
+ color: #374151;
+}
+
.subsection-title {
font-size: 14px;
font-weight: 700;
@@ -1648,6 +1698,12 @@ onMounted(async () => {
height: auto;
}
+ /* relation 每行卡片:打印时保留边框与布局 */
+ .flow-template-content :deep(.relation-row-card) {
+ border: 1px solid #000;
+ page-break-inside: avoid;
+ }
+
/* 打印文件展示样式 */
.flow-template-content :deep(.print-image img) {
max-width: 100%;
diff --git a/src/views/payment/PaymentQuery.vue b/src/views/payment/PaymentQuery.vue
index 2aaf198..efc6f48 100644
--- a/src/views/payment/PaymentQuery.vue
+++ b/src/views/payment/PaymentQuery.vue
@@ -582,8 +582,24 @@
@check-change="onConfirmTreeCheckChange"
/>
+
暂无数据
@@ -714,12 +730,14 @@ const confirmProjectTreeRef = ref(null)
const confirmSpecialFundTreeRef = ref(null)
const confirmLastYearCarryoverTreeRef = ref(null)
const confirmOffsetPrepaidTreeRef = ref(null)
+const confirmTradeUnionTreeRef = ref(null)
const confirmTreeRefs = {
department: confirmDepartmentTreeRef,
project: confirmProjectTreeRef,
specialFund: confirmSpecialFundTreeRef,
lastYear: confirmLastYearCarryoverTreeRef,
- offsetPrepaid: confirmOffsetPrepaidTreeRef
+ offsetPrepaid: confirmOffsetPrepaidTreeRef,
+ tradeUnion: confirmTradeUnionTreeRef
}
// 以下旧的预算来源修改对话框相关状态已废弃
@@ -760,6 +778,9 @@ const confirmLastYearCarryoverTree = computed(() => {
const confirmOffsetPrepaidTree = computed(() => {
return (confirmTreeData.value || []).filter((n) => n?.budget_type === 'offset_prepaid')
})
+const confirmTradeUnionTree = computed(() => {
+ return (confirmTreeData.value || []).filter((n) => n?.budget_type === 'trade_union')
+})
const confirmAllocationTotal = computed(() => {
return (confirmPendingAllocations.value || []).reduce((sum, item) => {
return sum + (parseFloat(item.allocated_amount) || 0)
@@ -1369,6 +1390,7 @@ const getBudgetTypeText = (budgetType) => {
if (budgetType === 'special_fund') return '专项资金'
if (budgetType === 'last_year_carryover') return '上一年结转资金'
if (budgetType === 'offset_prepaid') return '抵消预付账款'
+ if (budgetType === 'trade_union') return '工会'
return '部门预算'
}
@@ -1462,7 +1484,8 @@ const setConfirmCheckedIds = (ids) => {
confirmTreeRefs.project.value,
confirmTreeRefs.specialFund.value,
confirmTreeRefs.lastYear.value,
- confirmTreeRefs.offsetPrepaid.value
+ confirmTreeRefs.offsetPrepaid.value,
+ confirmTreeRefs.tradeUnion.value
].filter(Boolean)
refs.forEach((tree) => {
if (typeof tree.setCheckedKeys === 'function') {
@@ -1481,7 +1504,8 @@ const collectConfirmCheckedLeafNodes = () => {
confirmTreeRefs.project.value,
confirmTreeRefs.specialFund.value,
confirmTreeRefs.lastYear.value,
- confirmTreeRefs.offsetPrepaid.value
+ confirmTreeRefs.offsetPrepaid.value,
+ confirmTreeRefs.tradeUnion.value
].filter(Boolean)
const nodes = []
refs.forEach((tree) => {