Industrial-image-management.../src/views/project-management/contract/expense-contract/index.vue

701 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<GiPageLayout>
<GiTable
row-key="id"
title="支出合同管理"
:data="dataList"
:columns="tableColumns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1600 }"
:pagination="pagination"
@page-change="onPageChange"
@page-size-change="onPageSizeChange"
@refresh="search"
>
<template #top>
<GiForm
v-model="searchForm"
search
:columns="queryFormColumns"
size="medium"
@search="search"
@reset="reset"
/>
</template>
<template #toolbar-left>
<a-space>
<a-button type="primary" @click="openAddModal">
<template #icon><icon-plus /></template>
<template #default>新建合同</template>
</a-button>
<a-button @click="exportContract">
<template #icon><icon-download /></template>
<template #default>导出合同</template>
</a-button>
</a-space>
</template>
<!-- 合同状态 -->
<template #status="{ record }">
<a-tag :color="getStatusColor(record.contractStatus)">
{{ getStatusText(record.contractStatusLabel || record.contractStatus) }}
</a-tag>
</template>
<!-- 合同金额 -->
<template #contractAmount="{ record }">
<span class="font-medium text-green-600">¥{{ (record.amount || 0).toLocaleString() }}</span>
</template>
<!-- 已结算金额(支出合同) -->
<template #settlementAmount="{ record }">
<span class="font-medium text-blue-600">¥{{ (record.settlementAmount || record.receivedAmount || 0).toLocaleString() }}</span>
</template>
<template #action="{ record }">
<a-space>
<a-link @click="viewDetail(record)">查看</a-link>
<a-link @click="editRecord(record)">编辑</a-link>
<a-link :disabled="record.contractStatus === '已结算'" @click="!(record.contractStatus === '已结算') && openSettlement(record)">结算</a-link>
<a-link status="danger" @click="deleteContract(record)">删除</a-link>
</a-space>
</template>
<!-- 日期展示:仅年月日(放在 action 模板之外作为独立列插槽) -->
<template #signDate="{ record }">{{ (record.signDate || '').slice(0,10) }}</template>
<template #performanceDeadline="{ record }">{{ (record.performanceDeadline || '').slice(0,10) }}</template>
<template #paymentDate="{ record }">{{ (record.paymentDate || '').slice(0,10) }}</template>
</GiTable>
<!-- 合同详情弹窗 -->
<a-modal
v-model:visible="showDetailModal"
title="合同详情"
:width="800"
:footer="false"
@cancel="closeDetailModal"
>
<ContractDetail v-if="showDetailModal" :contract-id="selectedContractId" />
</a-modal>
<!-- 合同编辑弹窗 -->
<a-modal
v-model:visible="showEditModal"
title="编辑合同"
:width="800"
@cancel="closeEditModal"
@before-ok="handleEditSubmit"
>
<ContractEdit
v-if="showEditModal"
:contract-data="selectedContractData"
@update:contract-data="handleContractDataUpdate"
/>
</a-modal>
<!-- 新建合同弹窗 -->
<a-modal
v-model:visible="showAddModal"
title="新建合同"
:width="800"
@cancel="closeAddModal"
@before-ok="handleAddSubmit"
>
<ContractEdit
v-if="showAddModal"
:contract-data="newContractData"
@update:contract-data="handleNewContractDataUpdate"
/>
</a-modal>
<!-- 合同结算弹窗 -->
<a-modal v-model:visible="showSettlementModal" title="合同结算" :width="520" @before-ok="submitSettlement">
<a-form :model="settlementForm" layout="vertical">
<a-form-item field="amount" label="结算金额"><a-input-number v-model="settlementForm.amount" style="width:100%" /></a-form-item>
<a-form-item field="paymentDate" label="付款日期"><a-date-picker v-model="settlementForm.paymentDate" show-time value-format="YYYY-MM-DD HH:mm:ss" style="width:100%"/></a-form-item>
<a-form-item field="paymentPeriod" label="账期"><a-date-picker v-model="settlementForm.paymentPeriod" show-time value-format="YYYY-MM-DD HH:mm:ss" style="width:100%"/></a-form-item>
<a-form-item field="notes" label="备注"><a-textarea v-model="settlementForm.notes" /></a-form-item>
</a-form>
</a-modal>
</GiPageLayout>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { Message, Modal } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
import ContractEdit from './ContractEdit.vue'
import ContractDetail from './ContractDetail.vue'
import http from '@/utils/http'
// 接口数据类型定义
interface ContractItem {
contractId: string
customer: string
code: string
projectId: string
type: string
productService: string
paymentDate: string | null
performanceDeadline: string | null
paymentAddress: string
amount: number
accountNumber: string
notes: string
contractStatus: string
contractText: string | null
projectName: string
salespersonName: string | null
salespersonDeptName: string
settlementAmount: number | null
receivedAmount: number | null
contractStatusLabel: string | null
createBy: string | null
updateBy: string | null
createTime: string
updateTime: string
page: number
pageSize: number
signDate: string
duration: string
}
// 搜索表单
const searchForm = reactive({
contractCode: '',
client: '',
status: '',
signDateRange: [] as [string, string] | [],
page: 1,
size: 10,
})
// 查询条件配置
const queryFormColumns = [
{
field: 'client',
label: '客户',
type: 'input' as const,
props: {
placeholder: '请输入客户名称',
},
},
{
field: 'status',
label: '合同状态',
type: 'select' as const,
props: {
placeholder: '请选择合同状态',
options: [
{ label: '未执行', value: '未执行' },
{ label: '执行中', value: '执行中' },
{ label: '验收中', value: '验收中' },
{ label: '结算中', value: '结算中' },
{ label: '已结算', value: '已结算' },
{ label: '已终止', value: '已终止' },
],
},
},
{
field: 'signDateRange',
label: '签署时间',
type: 'range-picker' as const,
props: {
placeholder: ['开始日期', '结束日期'],
format: 'YYYY-MM-DD',
},
},
]
// 表格列配置
const tableColumns: TableColumnData[] = [
{ title: '合同编号', dataIndex: 'code', width: 150 },
{ title: '项目名称', dataIndex: 'projectName', width: 250, ellipsis: true, tooltip: true },
{ title: '客户名称', dataIndex: 'customer', width: 200, ellipsis: true, tooltip: true },
{ title: '合同金额', dataIndex: 'amount', slotName: 'contractAmount', width: 120 },
{ title: '已结算金额', dataIndex: 'settlementAmount', slotName: 'settlementAmount', width: 120 },
{ title: '未结算金额', dataIndex: 'pendingAmount', width: 120 },
{ title: '签署日期', dataIndex: 'signDate', slotName: 'signDate', width: 120 },
{ title: '履约期限', dataIndex: 'performanceDeadline', slotName: 'performanceDeadline', width: 120 },
{ title: '付款日期', dataIndex: 'paymentDate', slotName: 'paymentDate', width: 120 },
{ title: '合同状态', dataIndex: 'contractStatus', slotName: 'status', width: 100 },
{ title: '销售人员', dataIndex: 'salespersonName', width: 100 },
{ title: '销售部门', dataIndex: 'salespersonDeptName', width: 100 },
{ title: '产品服务', dataIndex: 'productService', width: 120, ellipsis: true, tooltip: true },
{ title: '备注', dataIndex: 'notes', width: 200, ellipsis: true, tooltip: true },
{ title: '操作', slotName: 'action', width: 200, fixed: 'right' },
]
// 数据状态
const loading = ref(false)
const dataList = ref<ContractItem[]>([])
// API调用函数
const fetchContractList = async () => {
try {
loading.value = true
const params = {
page: searchForm.page,
pageSize: searchForm.size,
code: searchForm.contractCode,
customer: searchForm.client,
contractStatus: searchForm.status,
signDateStart: Array.isArray(searchForm.signDateRange) && searchForm.signDateRange.length === 2 ? searchForm.signDateRange[0] : undefined,
signDateEnd: Array.isArray(searchForm.signDateRange) && searchForm.signDateRange.length === 2 ? searchForm.signDateRange[1] : undefined,
}
const response = await http.get('/contract/list', params)
if (response.code === 200) {
// 过滤出类型为"支出合同"的数据
const allContracts = response.rows || []
let filtered = allContracts.filter((item: ContractItem) => item.type === '支出合同')
// 如果后端未按时间段过滤,则在前端按签署时间范围再次过滤
const range = Array.isArray(searchForm.signDateRange) && searchForm.signDateRange.length === 2 ? searchForm.signDateRange : null
if (range) {
const [start, end] = range
const startTime = new Date(start as any).getTime()
const endTime = new Date(end as any).getTime()
if (!Number.isNaN(startTime) && !Number.isNaN(endTime)) {
filtered = filtered.filter((item: ContractItem) => {
if (!item.signDate) return false
const t = new Date(item.signDate as any).getTime()
return !Number.isNaN(t) && t >= startTime && t <= endTime
})
}
}
// 计算未结算金额(支出合同)
dataList.value = filtered.map((item: ContractItem) => ({
...item,
pendingAmount: (item.amount || 0) - ((item.settlementAmount || item.receivedAmount || 0)),
}))
// 更新分页总数(前端过滤后以过滤结果数为准)
pagination.total = dataList.value.length
} else {
Message.error(response.msg || '获取合同列表失败')
dataList.value = []
}
} catch (error) {
console.error('获取合同列表失败:', error)
Message.error('获取合同列表失败')
dataList.value = []
} finally {
loading.value = false
}
}
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showTotal: true,
showPageSize: true,
})
// 获取状态颜色
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
未执行: 'gray',
执行中: 'cyan',
验收中: 'arcoblue',
结算中: 'orange',
已结算: 'green',
已终止: 'red',
}
return colorMap[status] || 'gray'
}
// 获取状态文本
const getStatusText = (status: string) => {
// 直接返回后端返回的状态文本如果有contractStatusLabel则使用否则使用contractStatus
return status || '未知状态'
}
// 搜索和重置
const search = async () => {
await fetchContractList()
}
const reset = () => {
Object.assign(searchForm, {
contractCode: '',
client: '',
status: '',
signDateRange: [],
page: 1,
size: 10,
})
pagination.current = 1
search()
}
// 分页处理
const onPageChange = (page: number) => {
searchForm.page = page
pagination.current = page
search()
}
const onPageSizeChange = (size: number) => {
searchForm.size = size
searchForm.page = 1
pagination.pageSize = size
pagination.current = 1
search()
}
// 操作方法
const showAddModal = ref(false)
const newContractData = ref<ContractItem>({
contractId: '',
customer: '',
code: '',
projectId: '',
type: '支出合同',
productService: '',
paymentDate: null,
performanceDeadline: null,
paymentAddress: '',
amount: 0,
accountNumber: '',
notes: '',
contractStatus: '未确认',
contractText: '',
projectName: '',
salespersonName: null,
salespersonDeptName: '',
settlementAmount: null,
receivedAmount: null,
contractStatusLabel: null,
createBy: null,
updateBy: null,
createTime: '',
updateTime: '',
page: 1,
pageSize: 10,
signDate: '',
duration: '',
})
const openAddModal = () => {
// 重置默认值
newContractData.value = {
contractId: '',
customer: '',
code: '',
projectId: '',
type: '支出合同',
productService: '',
paymentDate: null,
performanceDeadline: null,
paymentAddress: '',
amount: 0,
accountNumber: '',
notes: '',
contractStatus: '未执行',
contractText: '',
projectName: '',
salespersonName: null,
salespersonDeptName: '',
settlementAmount: null,
receivedAmount: null,
contractStatusLabel: null,
createBy: null,
updateBy: null,
createTime: '',
updateTime: '',
page: 1,
pageSize: 10,
signDate: '',
duration: '',
}
showAddModal.value = true
}
const closeAddModal = () => {
showAddModal.value = false
}
const handleNewContractDataUpdate = (data: ContractItem) => {
// 不更换引用避免子组件watch到props变更导致重渲染丢失焦点
Object.assign(newContractData.value, data)
}
const handleAddSubmit = async () => {
try {
const payload: any = {
accountNumber: newContractData.value.accountNumber || '',
amount: newContractData.value.amount || 0,
code: newContractData.value.code || '',
contractStatus: newContractData.value.contractStatus || '',
contractText: newContractData.value.contractText || '',
customer: newContractData.value.customer || '',
departmentId: (newContractData.value as any).departmentId || '',
duration: newContractData.value.duration || '',
notes: newContractData.value.notes || '',
paymentAddress: newContractData.value.paymentAddress || '',
paymentDate: newContractData.value.paymentDate || null,
performanceDeadline: newContractData.value.performanceDeadline || null,
productService: newContractData.value.productService || '',
// 新建时不传 projectId而是传项目名称后续立项再建项目
projectName: newContractData.value.projectName || '',
salespersonId: (newContractData.value as any).salespersonId || '',
signDate: newContractData.value.signDate || null,
type: newContractData.value.type || '支出合同',
}
// 如果没有合同ID则不传
if (!newContractData.value.contractId) delete payload.contractId
const response = await http.post('/contract', payload)
if ((response as any).status === 200 && response.code === 200) {
Message.success('新建合同成功')
closeAddModal()
search()
return true
}
// 某些接口只返回 code/msg
if (response.code === 200) {
Message.success('新建合同成功')
closeAddModal()
search()
return true
}
Message.error(response.msg || '新建合同失败')
return false
} catch (error: any) {
console.error('新建合同失败:', error)
Message.error('新建合同失败: ' + (error?.message || '请稍后再试'))
return false
}
}
const exportContract = () => {
Message.info('导出合同功能开发中...')
}
// 显示合同编辑弹窗
const showEditModal = ref(false)
const selectedContractData = ref<ContractItem | null>(null)
const editedContractData = ref<ContractItem | null>(null)
const editRecord = (record: ContractItem) => {
// 确保所有必要的字段都存在
const completeRecord = {
...record,
amount: record.amount || 0,
projectId: record.projectId || '',
type: record.type || '支出合同',
contractStatus: record.contractStatus || '未执行',
}
selectedContractData.value = completeRecord
showEditModal.value = true
}
const closeEditModal = () => {
showEditModal.value = false
selectedContractData.value = null
editedContractData.value = null
}
const handleContractDataUpdate = (data: ContractItem) => {
// 避免替换引用导致子组件重建,保持编辑输入流畅
if (!editedContractData.value) {
editedContractData.value = { ...(data as any) } as ContractItem
} else {
Object.assign(editedContractData.value, data)
}
}
const handleEditSubmit = async () => {
if (!editedContractData.value) return false;
try {
const requestData = {
...editedContractData.value,
accountNumber: editedContractData.value.accountNumber || '',
amount: editedContractData.value.amount || 0,
code: editedContractData.value.code || '',
contractId: editedContractData.value.contractId,
contractStatus: editedContractData.value.contractStatus || '',
contractText: editedContractData.value.contractText || '',
customer: editedContractData.value.customer || '',
departmentId: editedContractData.value.departmentId || '',
duration: editedContractData.value.duration || '',
notes: editedContractData.value.notes || '',
paymentAddress: editedContractData.value.paymentAddress || '',
paymentDate: editedContractData.value.paymentDate || null,
performanceDeadline: editedContractData.value.performanceDeadline || null,
productService: editedContractData.value.productService || '',
projectId: editedContractData.value.projectId || '',
salespersonId: editedContractData.value.salespersonId || '',
signDate: editedContractData.value.signDate || null,
type: editedContractData.value.type || '',
};
// console.log('Edited Contract Data:', requestData); // 打印请求数据以便调试
// 修改此处,直接向 /contract 发送 PUT 请求
const response = await http.put('/contract', requestData);
// 检查响应状态
if (response.status === 200 && response.code === 200) {
Message.success('合同编辑成功');
closeEditModal();
search(); // 刷新列表
return true;
} else {
Message.error(response.msg || '合同编辑失败');
return false;
}
} catch (error) {
console.error('合同编辑失败:', error);
Message.error('合同编辑失败: ' + (error.message || '请稍后再试'));
return false;
}
}
// 合同结算
const showSettlementModal = ref(false)
const settlementForm = reactive({
// 核心字段
contractId: '',
amount: 0,
paymentDate: '',
paymentPeriod: '',
notes: '',
// 其余接口可选字段(用于兼容接口入参)
accountNumber: '',
code: '',
customer: '',
departmentId: '',
duration: '',
productService: '',
projectId: '',
salespersonId: '',
settlementId: '',
settlementStatus: '',
})
const settlementRecord = ref<ContractItem | null>(null)
const openSettlement = (record: ContractItem) => {
if (record.contractStatus === '已结算') return
settlementRecord.value = record
Object.assign(settlementForm, {
// 核心
contractId: record.contractId,
amount: record.amount || 0,
paymentDate: record.paymentDate || '',
paymentPeriod: '',
notes: '',
// 其余字段从合同记录回填,便于直接提交
accountNumber: (record as any).accountNumber || '',
code: (record as any).code || '',
customer: (record as any).customer || '',
departmentId: (record as any).departmentId || '',
duration: (record as any).duration || '',
productService: (record as any).productService || '',
projectId: (record as any).projectId || '',
salespersonId: (record as any).salespersonId || '',
settlementId: (record as any).settlementId || '',
settlementStatus: (record as any).settlementStatus || '',
})
showSettlementModal.value = true
}
const submitSettlement = async () => {
try {
const payload:any = {
accountNumber: settlementForm.accountNumber || '',
amount: settlementForm.amount,
code: settlementForm.code || '',
contractId: settlementForm.contractId,
customer: settlementForm.customer || '',
departmentId: settlementForm.departmentId || '',
duration: settlementForm.duration || '',
notes: settlementForm.notes || '',
paymentDate: settlementForm.paymentDate || null,
paymentPeriod: settlementForm.paymentPeriod || '',
productService: settlementForm.productService || '',
projectId: settlementForm.projectId || '',
salespersonId: settlementForm.salespersonId || '',
settlementId: settlementForm.settlementId || '',
settlementStatus: settlementForm.settlementStatus || '',
}
const res = await http.post('/contract-settlement', payload)
if (res.code === 200 || (res as any).status === 200) {
Message.success('合同结算成功')
// 根据未结算金额是否为 0 决定状态0 => 已结算,否则保留/置为“结算中”
const origin = settlementRecord.value
const settledAmount = (origin?.settlementAmount ?? origin?.receivedAmount ?? 0) + (settlementForm.amount || 0)
const pendingAfter = Math.max((origin?.amount || 0) - settledAmount, 0)
const targetStatus = pendingAfter === 0 ? '已结算' : '结算中'
await http.put('/contract', { contractId: settlementForm.contractId, contractStatus: targetStatus, type: origin?.type || '支出合同' })
showSettlementModal.value = false
search()
return true
}
Message.error(res.msg || '合同结算失败')
return false
} catch (e:any) {
Message.error(e?.message || '合同结算失败')
return false
}
}
// 删除合同:只有在确认框点击确定后才执行删除
const deleteContract = (record: ContractItem) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除合同 "${record.projectName}" 吗?`,
async onOk() {
try {
const response = await http.del(`/contract/${record.contractId}`)
if (response.code === 200) {
Message.success('合同删除成功')
search()
return true
} else {
Message.error(response.msg || '合同删除失败')
return false
}
} catch (error) {
console.error('合同删除失败:', error)
Message.error('合同删除失败')
return false
}
},
})
}
// 显示合同详情弹窗
const showDetailModal = ref(false)
const selectedContractId = ref<string | null>(null)
const viewDetail = (record: ContractItem) => {
selectedContractId.value = record.contractId
showDetailModal.value = true
}
const closeDetailModal = () => {
showDetailModal.value = false
selectedContractId.value = null
}
const approveContract = (record: ContractItem) => {
Message.info(`审批合同: ${record.projectName}`)
}
const viewPayment = (record: ContractItem) => {
Message.info(`查看收款记录: ${record.projectName}`)
}
onMounted(() => {
fetchContractList()
})
</script>