新增设备采购模块合同和发票上传

This commit is contained in:
Mr.j 2025-08-14 16:17:58 +08:00
parent c568370e54
commit 9e7cc674d4
2 changed files with 583 additions and 2 deletions

View File

@ -0,0 +1,534 @@
<template>
<a-modal
:visible="visible"
title="合同发票管理"
width="800px"
:footer="false"
@cancel="handleCancel"
@update:visible="handleVisibleChange"
>
<div class="contract-invoice-container">
<!-- 设备基本信息 -->
<a-card title="设备信息" class="info-card" :bordered="false">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="设备名称">
{{ equipmentData?.equipmentName }}
</a-descriptions-item>
<a-descriptions-item label="设备型号">
{{ equipmentData?.equipmentModel }}
</a-descriptions-item>
<a-descriptions-item label="供应商">
{{ equipmentData?.supplierName }}
</a-descriptions-item>
<a-descriptions-item label="采购价格">
¥{{ formatPrice(equipmentData?.purchasePrice || 0) }}
</a-descriptions-item>
</a-descriptions>
</a-card>
<!-- 合同信息 -->
<a-card title="合同信息" class="info-card" :bordered="false">
<a-form
ref="contractFormRef"
:model="contractForm"
:rules="contractRules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="合同编号" name="contractNumber">
<a-input
v-model="contractForm.contractNumber"
placeholder="请输入合同编号"
allow-clear
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="合同签订日期" name="contractDate">
<a-date-picker
v-model="contractForm.contractDate"
placeholder="请选择合同签订日期"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="合同金额" name="contractAmount">
<a-input-number
v-model="contractForm.contractAmount"
placeholder="请输入合同金额"
:precision="2"
:min="0"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="合同状态" name="contractStatus">
<a-select
v-model="contractForm.contractStatus"
placeholder="请选择合同状态"
allow-clear
>
<a-option value="DRAFT">草稿</a-option>
<a-option value="SIGNED">已签订</a-option>
<a-option value="EXECUTING">执行中</a-option>
<a-option value="COMPLETED">已完成</a-option>
<a-option value="TERMINATED">已终止</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="合同文件" name="contractFile">
<a-upload
v-model:file-list="contractForm.contractFile"
:action="uploadAction"
:headers="uploadHeaders"
:before-upload="beforeUpload"
:on-success="onContractUploadSuccess"
:on-error="onUploadError"
accept=".pdf,.doc,.docx"
:max-count="1"
>
<a-button>
<template #icon>
<IconUpload />
</template>
上传合同文件
</a-button>
<template #itemRender="{ file }">
<a-space>
<IconFile />
<span>{{ file.name }}</span>
<a-button
type="text"
size="small"
status="danger"
@click="removeContractFile(file)"
>
删除
</a-button>
</a-space>
</template>
</a-upload>
</a-form-item>
<a-form-item label="合同备注" name="contractRemark">
<a-textarea
v-model="contractForm.contractRemark"
placeholder="请输入合同备注信息"
:rows="3"
allow-clear
/>
</a-form-item>
</a-form>
</a-card>
<!-- 发票信息 -->
<a-card title="发票信息" class="info-card" :bordered="false">
<a-form
ref="invoiceFormRef"
:model="invoiceForm"
:rules="invoiceRules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="发票号码" name="invoiceNumber">
<a-input
v-model="invoiceForm.invoiceNumber"
placeholder="请输入发票号码"
allow-clear
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="发票类型" name="invoiceType">
<a-select
v-model="invoiceForm.invoiceType"
placeholder="请选择发票类型"
allow-clear
>
<a-option value="VAT_SPECIAL">增值税专用发票</a-option>
<a-option value="VAT_COMMON">增值税普通发票</a-option>
<a-option value="ELECTRONIC">电子发票</a-option>
<a-option value="OTHER">其他</a-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="发票金额" name="invoiceAmount">
<a-input-number
v-model="invoiceForm.invoiceAmount"
placeholder="请输入发票金额"
:precision="2"
:min="0"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="开票日期" name="invoiceDate">
<a-date-picker
v-model="invoiceForm.invoiceDate"
placeholder="请选择开票日期"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="税率" name="taxRate">
<a-input-number
v-model="invoiceForm.taxRate"
placeholder="请输入税率"
:precision="2"
:min="0"
:max="100"
style="width: 100%"
addon-after="%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="税额" name="taxAmount">
<a-input-number
v-model="invoiceForm.taxAmount"
placeholder="税额将自动计算"
:precision="2"
:min="0"
style="width: 100%"
disabled
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="发票文件" name="invoiceFile">
<a-upload
v-model:file-list="invoiceForm.invoiceFile"
:action="uploadAction"
:headers="uploadHeaders"
:before-upload="beforeUpload"
:on-success="onInvoiceUploadSuccess"
:on-error="onUploadError"
accept=".pdf,.jpg,.jpeg,.png"
:max-count="1"
>
<a-button>
<template #icon>
<IconUpload />
</template>
上传发票文件
</a-button>
<template #itemRender="{ file }">
<a-space>
<IconFile />
<span>{{ file.name }}</span>
<a-button
type="text"
size="small"
status="danger"
@click="removeInvoiceFile(file)"
>
删除
</a-button>
</a-space>
</template>
</a-upload>
</a-form-item>
<a-form-item label="发票备注" name="invoiceRemark">
<a-textarea
v-model="invoiceForm.invoiceRemark"
placeholder="请输入发票备注信息"
:rows="3"
allow-clear
/>
</a-form-item>
</a-form>
</a-card>
<!-- 操作按钮 -->
<div class="action-buttons">
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" @click="handleSave" :loading="saving">
保存
</a-button>
<a-button
type="primary"
@click="handleComplete"
:loading="saving"
:disabled="!canComplete"
>
完成并进入收货阶段
</a-button>
</a-space>
</div>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue'
import { IconUpload, IconFile } from '@arco-design/web-vue/es/icon'
import message from '@arco-design/web-vue/es/message'
import type { EquipmentResp } from '@/apis/equipment/type'
interface Props {
visible: boolean
equipmentData: EquipmentResp | null
}
interface ContractForm {
contractNumber: string
contractDate: string | null
contractAmount: number | null
contractStatus: string
contractFile: any[]
contractRemark: string
}
interface InvoiceForm {
invoiceNumber: string
invoiceType: string
invoiceAmount: number | null
invoiceDate: string | null
taxRate: number | null
taxAmount: number | null
invoiceFile: any[]
invoiceRemark: string
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:visible': [value: boolean]
'success': []
}>()
//
const contractFormRef = ref()
const invoiceFormRef = ref()
//
const saving = ref(false)
//
const contractForm = reactive<ContractForm>({
contractNumber: '',
contractDate: null,
contractAmount: null,
contractStatus: 'DRAFT',
contractFile: [],
contractRemark: ''
})
//
const invoiceForm = reactive<InvoiceForm>({
invoiceNumber: '',
invoiceType: '',
invoiceAmount: null,
invoiceDate: null,
taxRate: null,
taxAmount: null,
invoiceFile: [],
invoiceRemark: ''
})
//
const contractRules = {
contractNumber: [
{ required: true, message: '请输入合同编号', trigger: 'blur' }
],
contractDate: [
{ required: true, message: '请选择合同签订日期', trigger: 'change' }
],
contractAmount: [
{ required: true, message: '请输入合同金额', trigger: 'blur' }
],
contractStatus: [
{ required: true, message: '请选择合同状态', trigger: 'change' }
]
}
const invoiceRules = {
invoiceNumber: [
{ required: true, message: '请输入发票号码', trigger: 'blur' }
],
invoiceType: [
{ required: true, message: '请选择发票类型', trigger: 'change' }
],
invoiceAmount: [
{ required: true, message: '请输入发票金额', trigger: 'blur' }
],
invoiceDate: [
{ required: true, message: '请选择开票日期', trigger: 'change' }
],
taxRate: [
{ required: true, message: '请输入税率', trigger: 'blur' }
]
}
//
const uploadAction = '/api/file/upload'
const uploadHeaders = {
//
}
//
const canComplete = computed(() => {
return contractForm.contractStatus === 'SIGNED' &&
invoiceForm.invoiceNumber &&
invoiceForm.invoiceAmount
})
//
watch([() => invoiceForm.invoiceAmount, () => invoiceForm.taxRate], ([amount, rate]) => {
if (amount && rate) {
invoiceForm.taxAmount = Number((amount * rate / 100).toFixed(2))
} else {
invoiceForm.taxAmount = null
}
})
//
const formatPrice = (price: number) => {
return price.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
}
//
const beforeUpload = (file: File) => {
const isValidSize = file.size / 1024 / 1024 < 10 // 10MB
if (!isValidSize) {
message.error('文件大小不能超过10MB')
return false
}
return true
}
//
const onContractUploadSuccess = (response: any, file: any) => {
console.log('合同文件上传成功:', response, file)
message.success('合同文件上传成功')
}
//
const onInvoiceUploadSuccess = (response: any, file: any) => {
console.log('发票文件上传成功:', response, file)
message.success('发票文件上传成功')
}
//
const onUploadError = (error: any) => {
console.error('文件上传失败:', error)
message.error('文件上传失败')
}
//
const removeContractFile = (file: any) => {
const index = contractForm.contractFile.findIndex(f => f.uid === file.uid)
if (index > -1) {
contractForm.contractFile.splice(index, 1)
}
}
//
const removeInvoiceFile = (file: any) => {
const index = invoiceForm.invoiceFile.findIndex(f => f.uid === file.uid)
if (index > -1) {
invoiceForm.invoiceFile.splice(index, 1)
}
}
//
const handleCancel = () => {
emit('update:visible', false)
}
// visible
const handleVisibleChange = (newVisible: boolean) => {
emit('update:visible', newVisible)
}
//
const handleSave = async () => {
try {
saving.value = true
//
await contractFormRef.value?.validate()
//
await invoiceFormRef.value?.validate()
// API
console.log('保存合同发票信息:', {
contract: contractForm,
invoice: invoiceForm,
equipmentId: props.equipmentData?.equipmentId
})
message.success('保存成功')
} catch (error) {
console.error('保存失败:', error)
message.error('保存失败,请检查表单信息')
} finally {
saving.value = false
}
}
//
const handleComplete = async () => {
try {
saving.value = true
//
await contractFormRef.value?.validate()
await invoiceFormRef.value?.validate()
// API
console.log('完成合同发票管理:', {
contract: contractForm,
invoice: invoiceForm,
equipmentId: props.equipmentData?.equipmentId
})
message.success('合同发票管理完成,可以进入收货阶段')
emit('success')
emit('update:visible', false)
} catch (error) {
console.error('操作失败:', error)
message.error('操作失败,请检查表单信息')
} finally {
saving.value = false
}
}
</script>
<style scoped lang="scss">
.contract-invoice-container {
.info-card {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.action-buttons {
text-align: center;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid var(--color-border);
}
}
</style>

View File

@ -219,6 +219,15 @@
>
确认收货
</a-button>
<!-- 管理合同发票按钮 - 在采购审批通过后显示 -->
<a-button
v-if="canManageContractInvoice(record)"
type="outline"
size="small"
@click="handleManageContractInvoice(record)"
>
管理合同发票
</a-button>
<a-button
v-if="record.receiptStatus === 'RECEIVED'"
type="text"
@ -338,6 +347,13 @@
:equipment-data="currentPaymentData"
@success="handlePaymentSuccess"
/>
<!-- 合同发票管理弹窗 -->
<ContractInvoiceModal
v-model:visible="contractInvoiceModalVisible"
:equipment-data="currentContractInvoiceData"
@success="handleContractInvoiceSuccess"
/>
</div>
</template>
@ -361,6 +377,7 @@ import ReceiptDetailModal from './components/ReceiptDetailModal.vue'
import PaymentDetailModal from './components/PaymentDetailModal.vue'
import ReceiptModal from './components/ReceiptModal.vue'
import PaymentModal from './components/PaymentModal.vue'
import ContractInvoiceModal from './components/ContractInvoiceModal.vue'
import { equipmentProcurementApi } from '@/apis/equipment/procurement'
import { equipmentApprovalApi } from '@/apis/equipment/approval'
import type { EquipmentListReq, EquipmentResp } from '@/apis/equipment/type'
@ -409,6 +426,10 @@ const currentPaymentData = ref<EquipmentResp | null>(null)
const receiptModalVisible = ref(false)
const paymentModalVisible = ref(false)
//
const contractInvoiceModalVisible = ref(false)
const currentContractInvoiceData = ref<EquipmentResp | null>(null)
//
const selectedRowKeys = ref<string[]>([])
const rowSelection = reactive({
@ -1052,19 +1073,27 @@ const canApplyProcurement = (record: EquipmentResp) => {
//
const canReceiveGoods = (record: EquipmentResp) => {
//
//
const procurementStatus = record.procurementStatus
const receiptStatus = record.receiptStatus
//
// 使
const hasContractInvoice = true // true
console.log('🔍 canReceiveGoods 检查:', {
equipmentName: record.equipmentName,
procurementStatus,
receiptStatus,
hasContractInvoice,
canReceive: procurementStatus === 'APPROVED' &&
(receiptStatus === 'NOT_RECEIVED' || receiptStatus === 'PARTIALLY_RECEIVED')
(receiptStatus === 'NOT_RECEIVED' || receiptStatus === 'PARTIALLY_RECEIVED') &&
hasContractInvoice
})
return procurementStatus === 'APPROVED' &&
(receiptStatus === 'NOT_RECEIVED' || receiptStatus === 'PARTIALLY_RECEIVED')
(receiptStatus === 'NOT_RECEIVED' || receiptStatus === 'PARTIALLY_RECEIVED') &&
hasContractInvoice
}
//
@ -1160,6 +1189,24 @@ const handleRefreshRecord = async (record: EquipmentResp) => {
}
}
//
const canManageContractInvoice = (record: EquipmentResp) => {
//
return record.procurementStatus === 'APPROVED'
}
//
const handleManageContractInvoice = (record: EquipmentResp) => {
currentContractInvoiceData.value = { ...record }
contractInvoiceModalVisible.value = true
}
//
const handleContractInvoiceSuccess = () => {
contractInvoiceModalVisible.value = false
loadData(currentSearchParams.value)
}
onMounted(() => {
loadData()
})