weizong song 5 months ago
parent ad855925c6
commit 16fe061886

@ -34,11 +34,11 @@
<el-button
type="primary"
size="small"
@click="showSelectDialog = true"
@click="openSelectDialog('replace')"
:disabled="disabled"
>
<el-icon><Plus /></el-icon>
添加流程
<el-icon><Document /></el-icon>
重新选择
</el-button>
<el-button
type="danger"
@ -57,7 +57,7 @@
<el-button
type="primary"
size="small"
@click="showSelectDialog = true"
@click="openSelectDialog('replace')"
:disabled="disabled"
>
<el-icon><Document /></el-icon>
@ -104,7 +104,7 @@
<!-- 选择流程对话框 -->
<el-dialog
v-model="showSelectDialog"
title="选择事前流程实例"
:title="dialogTitle"
width="80%"
:close-on-click-modal="false"
@opened="handleDialogOpened"
@ -231,11 +231,12 @@
</div>
<template #footer>
<div class="dialog-footer-info">
<span v-if="selectedRows.length > 0">已选择 {{ selectedRows.length }} 条流程</span>
<span>{{ dialogFooterTip }}</span>
</div>
<div>
<el-button @click="showSelectDialog = false">取消</el-button>
<el-button type="primary" @click="confirmSelect" :disabled="selectedRows.length === 0">确定</el-button>
<el-button type="warning" @click="confirmSelect('replace')" :disabled="selectedRows.length === 0">确定替换</el-button>
<el-button type="primary" @click="confirmSelect('append')" :disabled="selectedRows.length === 0">确定追加</el-button>
</div>
</template>
</el-dialog>
@ -368,6 +369,7 @@ const searchKeyword = ref('')
const availableYears = ref([]) // 可用年份列表
const selectedYear = ref(null) // 选中的年份
const flowTableRef = ref(null) // 表格引用
const selectionMode = ref('replace') // replace=替换当前选择,append=追加到当前选择
// 分页
const pagination = ref({
@ -394,6 +396,23 @@ const detailFields = computed(() => {
return []
})
const dialogTitle = computed(() => {
return selectionMode.value === 'append' ? '追加事前流程实例' : '选择事前流程实例'
})
const dialogFooterTip = computed(() => {
const selectedCount = selectedRows.value.length
const existingCount = selectedFlows.value.length
return selectedRows.value.length > 0
? `本次勾选 ${selectedCount} 条。可选择替换已有流程,或追加到已有 ${existingCount} 条流程后面(重复自动忽略)。`
: `请选择流程。可替换已有流程,或追加到已有 ${existingCount} 条流程后面。`
})
const openSelectDialog = (mode = 'replace') => {
selectionMode.value = mode
showSelectDialog.value = true
}
// 获取流程显示名称
const getFlowDisplayName = (flow) => {
if (!flow) return ''
@ -772,14 +791,18 @@ const handleSelectionChange = (selection) => {
}
// 确认选择(多选)
const confirmSelect = () => {
const confirmSelect = (mode = selectionMode.value) => {
if (selectedRows.value.length === 0) {
ElMessage.warning('请至少选择一条流程')
return
}
const nextSelectedFlows = mode === 'append'
? dedupeFlows([...selectedFlows.value, ...selectedRows.value])
: [...selectedRows.value]
// 更新已选择的流程列表
selectedFlows.value = [...selectedRows.value]
selectedFlows.value = nextSelectedFlows
// 提取流程ID数组
const flowIds = selectedFlows.value.map(flow => flow.id)
@ -792,6 +815,23 @@ const confirmSelect = () => {
showSelectDialog.value = false
}
const dedupeFlows = (flows) => {
const seen = new Set()
const result = []
flows.forEach(flow => {
if (!flow || flow.id === null || flow.id === undefined) {
return
}
const key = String(flow.id)
if (seen.has(key)) {
return
}
seen.add(key)
result.push(flow)
})
return result
}
// 移除单个流程
const removeFlow = (flowId) => {
selectedFlows.value = selectedFlows.value.filter(flow => flow.id !== flowId)

@ -303,7 +303,18 @@
>
<template #header>
<div class="drawer-header">
<h3>{{ currentBudgetName }}</h3>
<div class="drawer-title-row">
<h3>{{ currentBudgetName }}</h3>
<el-button
type="primary"
size="small"
:icon="Download"
:disabled="!executionDetails.executed.length && !executionDetails.pending.length"
@click="exportExecutionDetails"
>
导出明细
</el-button>
</div>
<div class="drawer-summary">
<div class="summary-item">
<span class="label">已执行:</span>
@ -525,7 +536,9 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import * as XLSX from 'xlsx'
import {
DataAnalysis,
Search,
@ -536,11 +549,14 @@ import {
Folder,
Document,
View,
Download,
ArrowDown,
ArrowRight
} from '@element-plus/icons-vue'
import { budgetDataAPI, paymentDetailAPI } from '@/utils/api'
const router = useRouter()
const filterForm = ref({
year_id: ''
})
@ -931,6 +947,72 @@ const loadExecutionDetails = async (budgetId) => {
}
}
const sanitizeFileName = (name) => {
return String(name || '预算明细')
.replace(/[\\/:*?"<>|]/g, '_')
.slice(0, 80)
}
const formatExportDateTime = (value) => {
if (!value) return ''
if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$/.test(value)) {
return value
}
const date = new Date(value)
if (Number.isNaN(date.getTime())) return String(value)
const pad = (num) => String(num).padStart(2, '0')
const year = date.getFullYear()
const month = pad(date.getMonth() + 1)
const day = pad(date.getDate())
const hours = pad(date.getHours())
const minutes = pad(date.getMinutes())
const seconds = pad(date.getSeconds())
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
const mapExecutionRows = (rows, statusText) => {
return (rows || []).map((row, index) => ({
序号: index + 1,
状态: statusText || row.payment_status_text || '',
支付单号: row.payment_serial_number || '',
支付日期: formatExportDateTime(row.payment_date),
收款单位: row.payee || '',
支付事项: row.payment_item || row.description || '',
金额: Number(row.amount || 0),
付款ID: row.payment_id || '',
分摊ID: row.allocation_id || '',
创建时间: formatExportDateTime(row.created_at)
}))
}
const exportExecutionDetails = () => {
const executedRows = executionDetails.value.executed || []
const pendingRows = executionDetails.value.pending || []
if (!executedRows.length && !pendingRows.length) {
ElMessage.warning('暂无可导出的明细')
return
}
const workbook = XLSX.utils.book_new()
const summaryRows = [
{ 项目: '预算节点', 数值: currentBudgetName.value || '' },
{ 项目: '已执行合计', 数值: Number(executionDetails.value.executed_total || 0) },
{ 项目: '待执行合计', 数值: Number(executionDetails.value.pending_total || 0) },
{ 项目: '导出时间', 数值: new Date().toLocaleString('zh-CN') }
]
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(summaryRows), '汇总')
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(mapExecutionRows(executedRows, '已执行')), '已执行')
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(mapExecutionRows(pendingRows, '待执行')), '待执行')
const date = new Date().toISOString().slice(0, 10)
XLSX.writeFile(workbook, `${sanitizeFileName(currentBudgetName.value)}_执行明细_${date}.xlsx`)
ElMessage.success('导出成功')
}
const buildDisplayTree = (nodes) => {
return nodes.map(node => ({
...node,
@ -1603,6 +1685,13 @@ onMounted(async () => {
gap: 15px;
}
.drawer-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.drawer-header h3 {
margin: 0;
font-size: 18px;

@ -2092,6 +2092,10 @@ const formData = ref({})
// 将 CSV 字符串转换为数组(用于组件)
const getFlowArrayValue = (fieldKey) => {
const value = formData.value[fieldKey]
return parseFlowIdsFromValue(value)
}
const parseFlowIdsFromValue = (value) => {
if (!value) return null
if (Array.isArray(value)) return value
if (typeof value === 'string' && value.includes(',')) {
@ -2108,23 +2112,26 @@ const getFlowArrayValue = (fieldKey) => {
// 处理流程值变化(数组转 CSV)
const handleFlowValueChange = async (fieldKey, flowIds) => {
console.log('handleFlowValueChange 被调用:', { fieldKey, flowIds })
const prevFlowIds = parseFlowIdsFromValue(formData.value[fieldKey]) || []
const nextFlowIds = Array.isArray(flowIds) ? dedupeFlowIds(flowIds) : []
const removedFlowIds = diffFlowIds(prevFlowIds, nextFlowIds)
if (!flowIds || flowIds.length === 0) {
if (nextFlowIds.length === 0) {
formData.value[fieldKey] = null
// 清空relation数据
clearRelationDataForField(fieldKey)
clearRelationDataForFlowIds(removedFlowIds.length > 0 ? removedFlowIds : prevFlowIds)
} else {
// 转换为 CSV 字符串
formData.value[fieldKey] = flowIds.map(id => String(id)).join(',')
formData.value[fieldKey] = nextFlowIds.map(id => String(id)).join(',')
clearRelationDataForFlowIds(removedFlowIds)
console.log('流程ID已设置:', formData.value[fieldKey])
// 智能填充:从第一个流程实例中提取项目名称
await smartFillProjectName(flowIds[0])
await smartFillProjectName(nextFlowIds[0])
// 先加载流程详情到 flowDetailsMap(用于显示流程信息)
try {
const flowDetailsResponse = await plannedExpenditureAPI.getOaFlowDetails(flowIds)
const flowDetailsResponse = await plannedExpenditureAPI.getOaFlowDetails(nextFlowIds)
if (flowDetailsResponse.code === 0 && flowDetailsResponse.data) {
Object.keys(flowDetailsResponse.data).forEach(flowId => {
flowDetailsMap.value[flowId] = flowDetailsResponse.data[flowId]
@ -2136,11 +2143,33 @@ const handleFlowValueChange = async (fieldKey, flowIds) => {
// 加载relation字段数据
console.log('开始加载relation字段数据...')
await loadRelationFieldsData(flowIds, fieldKey)
await loadRelationFieldsData(nextFlowIds, fieldKey)
console.log('relation字段数据加载完成,当前数据:', relationFieldsData.value)
}
}
const dedupeFlowIds = (flowIds) => {
const seen = new Set()
const result = []
flowIds.forEach(id => {
if (id === null || id === undefined || id === '') {
return
}
const key = String(id)
if (seen.has(key)) {
return
}
seen.add(key)
result.push(id)
})
return result
}
const diffFlowIds = (oldIds, newIds) => {
const newIdSet = new Set((newIds || []).map(id => String(id)))
return (oldIds || []).filter(id => !newIdSet.has(String(id)))
}
// 清空指定字段的relation数据
const clearRelationDataForField = (fieldKey) => {
// 找到对应的流程ID(从formData中获取)
@ -2148,7 +2177,14 @@ const clearRelationDataForField = (fieldKey) => {
if (!flowIdsStr) {
return
}
const flowIds = flowIdsStr.split(',').map(id => id.trim()).filter(id => id)
const flowIds = parseFlowIdsFromValue(flowIdsStr) || []
clearRelationDataForFlowIds(flowIds)
}
const clearRelationDataForFlowIds = (flowIds) => {
if (!flowIds || flowIds.length === 0) {
return
}
flowIds.forEach(flowId => {
Object.keys(relationFieldsData.value).forEach(key => {
if (key.startsWith(`${flowId}_`)) {
@ -4360,6 +4396,64 @@ const loadFlowDetailsForPreview = async () => {
}
}
const buildDraftPayload = () => ({
category_id: selectedCategory.value?.id,
template_id: selectedTemplate.value?.id,
form_data: formData.value,
save_action: 'draft',
contract_id: allowsContractSelection(selectedCategory.value) && selectedExpenditureContract.value
? selectedExpenditureContract.value.id
: null
})
const syncDraftIdToRoute = async () => {
if (!draftId.value || String(route.query.draftId || '') === String(draftId.value)) {
return
}
await router.replace({
path: route.path,
query: {
...route.query,
draftId: draftId.value
}
})
}
const saveDraftSilently = async () => {
const submitData = buildDraftPayload()
let response
if (draftId.value) {
response = await plannedExpenditureAPI.update(draftId.value, submitData)
} else {
response = await plannedExpenditureAPI.create(submitData)
if (response.code === 0 && response.data?.id) {
draftId.value = response.data.id
}
}
if (response.code !== 0) {
throw new Error(response.msg || '暂存失败')
}
await syncDraftIdToRoute()
return response
}
const ensureDraftBeforeMainFlowStep = async () => {
saving.value = true
try {
const wasCreated = !draftId.value
await saveDraftSilently()
ElMessage.success(wasCreated ? '已自动暂存,进入发起主流程' : '草稿已更新,进入发起主流程')
return true
} catch (error) {
ElMessage.error('进入发起主流程前自动暂存失败:' + (error.message || '未知错误'))
return false
} finally {
saving.value = false
}
}
// 下一步
const nextStep = async () => {
// 步骤1:验证是否选择了第一层分类
@ -4638,6 +4732,11 @@ const nextStep = async () => {
// 步骤6:总体预览,进入下一步
if (currentStep.value === 6) {
const draftReady = await ensureDraftBeforeMainFlowStep()
if (!draftReady) {
return
}
// 查找主审批流程
paymentMainApprovalFlow.value = paymentTemplateElements.value.find(
el => el.type === 'approval_flow' && el.is_main_approval_process
@ -5826,6 +5925,9 @@ const paymentOaIframeUrl = computed(() => {
if (token) {
params.set('auth_token', token)
}
if (draftId.value) {
params.set('budget_draft_id', String(draftId.value))
}
return `${baseUrl}?${params.toString()}`
})
@ -6018,6 +6120,12 @@ const autoFinalSubmit = async () => {
// 重置失败状态
autoSubmitFailed.value = false
if (!draftId.value) {
ElMessage.warning('请先从总体预览进入发起主流程,系统会自动暂存后再创建付款')
autoSubmitFailed.value = true
return
}
// 验证流程ID
if (!paymentOaFlowId.value) {
@ -6100,6 +6208,7 @@ const autoFinalSubmit = async () => {
// 构建统一提交数据
const submitData = {
expenditure: {
id: draftId.value || undefined,
category_id: selectedCategory.value?.id,
template_id: selectedTemplate.value?.id,
form_data: formData.value,
@ -6115,59 +6224,17 @@ const autoFinalSubmit = async () => {
}
}
// 如果有草稿ID,需要先更新草稿,然后再创建支付
if (draftId.value) {
const updateResponse = await plannedExpenditureAPI.update(draftId.value, {
category_id: selectedCategory.value?.id,
template_id: selectedTemplate.value?.id,
form_data: formData.value,
save_action: 'submit',
contract_id: expenditureContractId
})
if (updateResponse.code !== 0) {
ElMessage.error(`已创建OA流程(流程ID: ${paymentOaFlowId.value}),但更新支出记录失败:${updateResponse.msg || '未知错误'}`)
autoSubmitFailed.value = true
lastAutoFlowId.value = null
return
}
const paymentData = {
payment_category_id: selectedPaymentCategoryId.value,
amount: amountNumber,
fields: paymentFormData.fields,
oa_flow_id: paymentOaFlowId.value,
// Step2:后端已切换 1 Payment = 1 明细,直接写 Payment 级字段(不再传 details)
expenditure_type: 'indirect',
expenditure_id: draftId.value,
contract_id: paymentDetailContractId,
writeoff_details: writeoffDetails
}
const paymentResponse = await paymentAPI.create(paymentData)
if (paymentResponse.code === 0) {
// 成功:停留本页,不弹窗提示,不自动跳转(避免影响抽屉)
submitSuccess.value = true
submitSuccessPayload.value = paymentResponse.data || null
} else {
ElMessage.error(`已创建OA流程(流程ID: ${paymentOaFlowId.value}),但创建支付单失败:${paymentResponse.msg || '未知错误'}`)
autoSubmitFailed.value = true
lastAutoFlowId.value = null
}
// 使用统一接口创建非直接支付;有草稿时由后端在同一事务内更新草稿并创建 Payment。
const response = await indirectPaymentAPI.create(submitData)
if (response.code === 0) {
// 成功:停留本页,不弹窗提示,不自动跳转(避免影响抽屉)
submitSuccess.value = true
submitSuccessPayload.value = response.data || null
} else {
// 新创建:使用统一接口(事务保证)
const response = await indirectPaymentAPI.create(submitData)
if (response.code === 0) {
// 成功:停留本页,不弹窗提示,不自动跳转(避免影响抽屉)
submitSuccess.value = true
submitSuccessPayload.value = response.data || null
} else {
ElMessage.error(`已创建OA流程(流程ID: ${paymentOaFlowId.value}),但创建支付单失败:${response.msg || '未知错误'}`)
autoSubmitFailed.value = true
lastAutoFlowId.value = null
}
ElMessage.error(`已创建OA流程(流程ID: ${paymentOaFlowId.value}),但创建支付单失败:${response.msg || '未知错误'}`)
autoSubmitFailed.value = true
lastAutoFlowId.value = null
}
} catch (error) {
ElMessage.error(`已创建OA流程(流程ID: ${paymentOaFlowId.value}),但创建支付单失败:${error.message}`)
@ -6180,6 +6247,11 @@ const autoFinalSubmit = async () => {
// 最终提交(第7步)
const handleFinalSubmit = async () => {
if (!draftId.value) {
ElMessage.warning('请先从总体预览进入发起主流程,系统会自动暂存后再创建付款')
return
}
// 无论是否配置主流程,都必须提供流程ID
if (!paymentOaFlowId.value) {
ElMessage.warning('请先在OA页面完成主审批流程的创建')
@ -6281,6 +6353,7 @@ const handleFinalSubmit = async () => {
// 构建统一提交数据
const submitData = {
expenditure: {
id: draftId.value || undefined,
category_id: selectedCategory.value?.id,
template_id: selectedTemplate.value?.id,
form_data: formData.value,
@ -6296,62 +6369,15 @@ const handleFinalSubmit = async () => {
}
}
// 如果有草稿ID,需要先更新草稿,然后再创建支付
// 但为了保持事务一致性,我们仍然使用统一接口
// 注意:这里需要后端支持更新草稿的逻辑,或者前端先更新草稿再调用统一接口
// 暂时先处理新创建的情况
if (draftId.value) {
// 如果有草稿,先更新草稿为提交状态
const updateResponse = await plannedExpenditureAPI.update(draftId.value, {
category_id: selectedCategory.value?.id,
template_id: selectedTemplate.value?.id,
form_data: formData.value,
save_action: 'submit',
contract_id: expenditureContractId
})
if (updateResponse.code !== 0) {
ElMessage.error('更新支出记录失败:' + (updateResponse.msg || '未知错误'))
submitting.value = false
return
}
// 然后创建支付流程(单独创建,因为支出记录已经存在)
const paymentData = {
payment_category_id: selectedPaymentCategoryId.value,
amount: amountNumber,
fields: paymentFormData.fields,
oa_flow_id: paymentOaFlowId.value,
// Step2:后端已切换 1 Payment = 1 明细,直接写 Payment 级字段(不再传 details)
expenditure_type: 'indirect',
expenditure_id: draftId.value,
contract_id: paymentDetailContractId,
// StepX:统一关联字段(后端:related_type/related_id)
related_type: 'planned_expenditure',
related_id: draftId.value,
writeoff_details: writeoffDetails
}
const paymentResponse = await paymentAPI.create(paymentData)
if (paymentResponse.code === 0) {
// 成功:停留本页,不弹窗提示,不自动跳转(避免影响抽屉)
submitSuccess.value = true
submitSuccessPayload.value = paymentResponse.data || null
} else {
ElMessage.error(paymentResponse.msg || '创建支付流程失败')
}
// 使用统一接口创建非直接支付;有草稿时由后端在同一事务内更新草稿并创建 Payment。
const response = await indirectPaymentAPI.create(submitData)
if (response.code === 0) {
// 成功:停留本页,不弹窗提示,不自动跳转(避免影响抽屉)
submitSuccess.value = true
submitSuccessPayload.value = response.data || null
} else {
// 新创建:使用统一接口(事务保证)
const response = await indirectPaymentAPI.create(submitData)
if (response.code === 0) {
// 成功:停留本页,不弹窗提示,不自动跳转(避免影响抽屉)
submitSuccess.value = true
submitSuccessPayload.value = response.data || null
} else {
ElMessage.error(response.msg || '创建失败')
}
ElMessage.error(response.msg || '创建失败')
}
} catch (error) {
if (error !== 'cancel') {
@ -6368,30 +6394,8 @@ const handleFinalSubmit = async () => {
const handleSaveDraft = async () => {
savingDraft.value = true
try {
const submitData = {
category_id: selectedCategory.value?.id,
template_id: selectedTemplate.value?.id,
form_data: formData.value,
save_action: 'draft'
}
let response
if (draftId.value) {
// 更新草稿
response = await plannedExpenditureAPI.update(draftId.value, submitData)
} else {
// 创建新草稿
response = await plannedExpenditureAPI.create(submitData)
if (response.code === 0 && response.data?.id) {
draftId.value = response.data.id
}
}
if (response.code === 0) {
ElMessage.success('已暂存')
} else {
ElMessage.error('暂存失败:' + (response.msg || '未知错误'))
}
await saveDraftSilently()
ElMessage.success('已暂存')
} catch (error) {
ElMessage.error('暂存失败:' + error.message)
} finally {

@ -213,13 +213,7 @@ const handleView = (row) => {
}
const handleCreatePayment = async (row) => {
// 跳转到申请付款页面,传递expenditure_id(会自动判断支付类型)
router.push({
path: '/payment/create-payment',
query: {
expenditure_id: row.id
}
})
ElMessage.warning('非直接支付已统一在发起页面完成付款创建,不能从流转查询单独创建付款')
}
const handleConfirmCreatePayment = async () => {
@ -236,15 +230,9 @@ const handleConfirmCreatePayment = async () => {
creatingPayment.value = true
try {
// Step2:支付创建需要 OA 主流程(oa_flow_id),本页仅作为“引导入口”
// 直接跳转到申请付款页,由其完成流程创建与提交
// 非直接支付已统一在发起页面完成付款创建,本页不再作为旧付款入口。
paymentDialogVisible.value = false
router.push({
path: '/payment/create-payment',
query: {
expenditure_id: currentExpenditure.value.id
}
})
ElMessage.warning('非直接支付已统一在发起页面完成付款创建,不能从流转查询单独创建付款')
} catch (error) {
console.error('创建支付流程失败:', error)
ElMessage.error('创建支付流程失败:' + (error.message || '未知错误'))

Loading…
Cancel
Save