|
|
@ -1,140 +1,179 @@ |
|
|
|
<template> |
|
|
|
<div class="h-full"> |
|
|
|
<div class="modal-header"> |
|
|
|
<h2 class="modal-title">编辑合同产品信息</h2> |
|
|
|
<div class="modal-actions"> |
|
|
|
<el-button type="primary" @click="handleSubmit">保存</el-button> |
|
|
|
<div class="container"> |
|
|
|
<div class="header"> |
|
|
|
<h2 class="title">编辑合同产品信息</h2> |
|
|
|
<div class="actions"> |
|
|
|
<el-button type="primary" @click="handleAddProduct">新增产品</el-button> |
|
|
|
<el-button @click="handleClose">关闭</el-button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div class="grid grid-cols-2 gap-2 h-full"> |
|
|
|
<div class="content"> |
|
|
|
<!-- 左侧PDF显示区域 --> |
|
|
|
<div class="col-span-1 h-full overflow-auto"> |
|
|
|
<embed :src="pdfUrl" type="application/pdf" width="100%" height="100%" class="pdf-embed" /> |
|
|
|
<div class="pdf-area"> |
|
|
|
<embed :src="pdfUrl" type="application/pdf" width="100%" height="100%" /> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 右侧编辑表单区域 --> |
|
|
|
<div class="h-full overflow-auto"> |
|
|
|
<el-form |
|
|
|
ref="formRef" |
|
|
|
:model="formData" |
|
|
|
:rules="rules" |
|
|
|
label-width="100px" |
|
|
|
class="edit-form" |
|
|
|
> |
|
|
|
<el-form-item label="品牌" prop="brand"> |
|
|
|
<el-input v-model="formData.brand" placeholder="请输入品牌" /> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="型号(版本号)" prop="versionStr"> |
|
|
|
<el-input v-model="formData.versionStr" placeholder="请输入型号" /> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="CPU型号" prop="cpuModel"> |
|
|
|
<el-input v-model="formData.cpuModel" placeholder="请输入CPU型号" /> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="类型" prop="type"> |
|
|
|
<el-input v-model="formData.type" placeholder="请输入类型" /> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="单价" prop="unitPrice"> |
|
|
|
<el-input-number |
|
|
|
v-model="formData.unitPrice" |
|
|
|
:precision="0" |
|
|
|
:step="1" |
|
|
|
:min="0" |
|
|
|
placeholder="请输入单价" |
|
|
|
/> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="数量" prop="quantity"> |
|
|
|
<el-input-number |
|
|
|
v-model="formData.quantity" |
|
|
|
:min="0" |
|
|
|
placeholder="请输入数量" |
|
|
|
/> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="总价" prop="totalPrice"> |
|
|
|
<el-input-number |
|
|
|
v-model="formData.totalPrice" |
|
|
|
:precision="0" |
|
|
|
:step="1" |
|
|
|
:min="0" |
|
|
|
placeholder="总价" |
|
|
|
disabled |
|
|
|
/> |
|
|
|
</el-form-item> |
|
|
|
</el-form> |
|
|
|
<div class="form-area"> |
|
|
|
<!-- 产品列表 --> |
|
|
|
<div v-for="(product, index) in productList" :key="product.id || index" class="product-item"> |
|
|
|
<div class="product-header"> |
|
|
|
<h3>产品 #{{ index + 1 }}</h3> |
|
|
|
<div class="product-actions"> |
|
|
|
<el-button type="primary" size="small" @click="handleSubmitSingle(index)">保存</el-button> |
|
|
|
<el-button type="danger" size="small" @click="handleRemoveProduct(index)" v-if="productList.length > 1"> |
|
|
|
删除 |
|
|
|
</el-button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<el-form |
|
|
|
:ref="el => (formRefs[index] = el)" |
|
|
|
:model="product" |
|
|
|
label-width="100px" |
|
|
|
class="edit-form" |
|
|
|
> |
|
|
|
<el-form-item label="品牌" prop="brand"> |
|
|
|
<el-input v-model="product.brand" placeholder="请输入品牌" /> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="型号(版本号)" prop="versionStr"> |
|
|
|
<el-input v-model="product.versionStr" placeholder="请输入型号" /> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="CPU型号" prop="cpuModel"> |
|
|
|
<el-input v-model="product.cpuModel" placeholder="请输入CPU型号" /> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="类型" prop="type"> |
|
|
|
<el-input v-model="product.type" placeholder="请输入类型" /> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="单价" prop="unitPrice"> |
|
|
|
<el-input-number |
|
|
|
v-model="product.unitPrice" |
|
|
|
:precision="0" |
|
|
|
:step="1" |
|
|
|
:min="0" |
|
|
|
placeholder="请输入单价" |
|
|
|
/> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="数量" prop="quantity"> |
|
|
|
<el-input-number |
|
|
|
v-model="product.quantity" |
|
|
|
:min="0" |
|
|
|
placeholder="请输入数量" |
|
|
|
/> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="总价" prop="totalPrice"> |
|
|
|
<el-input-number |
|
|
|
v-model="product.totalPrice" |
|
|
|
:precision="0" |
|
|
|
:step="1" |
|
|
|
:min="0" |
|
|
|
placeholder="总价" |
|
|
|
disabled |
|
|
|
/> |
|
|
|
</el-form-item> |
|
|
|
</el-form> |
|
|
|
<el-divider v-if="index < productList.length - 1" /> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
|
|
|
|
<script setup lang="ts"> |
|
|
|
import { ref, reactive, onMounted, nextTick, watch } from 'vue'; |
|
|
|
import { ref, reactive, onMounted, nextTick, watch, computed } from 'vue'; |
|
|
|
import { ElMessage } from 'element-plus'; |
|
|
|
import { updateContractualProductInfo } from '@/api/contractReview/ContractualProductInfo'; |
|
|
|
import { updateContractualProductInfo,addContractualProductInfo } from '@/api/contractReview/ContractualProductInfo'; |
|
|
|
import { getPdfFile } from '@/api/contractReview/JyjcontractualTaskBatch'; |
|
|
|
|
|
|
|
defineOptions({ name: 'ContractualProductInfoEdit' }); |
|
|
|
|
|
|
|
// 定义产品类型接口 |
|
|
|
interface ProductInfo { |
|
|
|
id: string | number; |
|
|
|
taskId: string | number; |
|
|
|
fileName: string; |
|
|
|
unitName: string; |
|
|
|
brand: string; |
|
|
|
versionStr: string; |
|
|
|
cpuModel: string; |
|
|
|
type: string; |
|
|
|
unitPrice: number; |
|
|
|
quantity: number; |
|
|
|
totalPrice: number; |
|
|
|
} |
|
|
|
|
|
|
|
// 定义Props |
|
|
|
const props = defineProps({ |
|
|
|
record: { |
|
|
|
type: Object, |
|
|
|
type: Array as () => ProductInfo[], |
|
|
|
required: true, |
|
|
|
default: () => ({}) |
|
|
|
default: () => [] |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 定义Emits |
|
|
|
const emit = defineEmits(['close', 'success']); |
|
|
|
|
|
|
|
// 表单数据和校验规则 |
|
|
|
const formRef = ref(); |
|
|
|
const formData = reactive({ |
|
|
|
id: '', |
|
|
|
taskId: '', |
|
|
|
fileName: '', |
|
|
|
unitName: '', |
|
|
|
brand: '', |
|
|
|
versionStr: '', |
|
|
|
cpuModel: '', |
|
|
|
type: '', |
|
|
|
unitPrice: 0, |
|
|
|
quantity: 0, |
|
|
|
totalPrice: 0 |
|
|
|
}); |
|
|
|
|
|
|
|
// 校验规则 |
|
|
|
const rules = { |
|
|
|
brand: [{ required: true, message: '请输入品牌', trigger: 'blur' }], |
|
|
|
// 创建空白产品模板 |
|
|
|
const createEmptyProduct = (): ProductInfo => { |
|
|
|
return { |
|
|
|
id: '', |
|
|
|
taskId: props.record.length > 0 ? props.record[0].taskId : '', |
|
|
|
fileName: props.record.length > 0 ? props.record[0].fileName : '', |
|
|
|
unitName: '', |
|
|
|
brand: '', |
|
|
|
versionStr: '', |
|
|
|
cpuModel: '', |
|
|
|
type: '', |
|
|
|
unitPrice: 0, |
|
|
|
quantity: 0, |
|
|
|
totalPrice: 0 |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
// 产品列表 |
|
|
|
const productList = ref<ProductInfo[]>([]); |
|
|
|
|
|
|
|
// 表单引用数组 |
|
|
|
const formRefs = ref<any[]>([]); |
|
|
|
|
|
|
|
// PDF文件URL |
|
|
|
const pdfUrl = ref<string>(''); |
|
|
|
const loading = ref(false); |
|
|
|
|
|
|
|
// 监听单价和数量的变化,计算总价 |
|
|
|
watch( |
|
|
|
() => [formData.unitPrice, formData.quantity], |
|
|
|
([newUnitPrice, newQuantity]) => { |
|
|
|
formData.totalPrice = newUnitPrice * newQuantity; |
|
|
|
}, |
|
|
|
{ immediate: true } |
|
|
|
); |
|
|
|
// 监听每个产品的单价和数量变化,计算总价 |
|
|
|
watch(productList, () => { |
|
|
|
productList.value.forEach(product => { |
|
|
|
product.totalPrice = product.unitPrice * product.quantity; |
|
|
|
}); |
|
|
|
}, { deep: true }); |
|
|
|
|
|
|
|
// 初始化数据 |
|
|
|
onMounted(async () => { |
|
|
|
if (props.record) { |
|
|
|
// 填充表单数据 |
|
|
|
Object.keys(formData).forEach(key => { |
|
|
|
if (props.record[key] !== undefined) { |
|
|
|
formData[key] = props.record[key]; |
|
|
|
} |
|
|
|
if (props.record && props.record.length) { |
|
|
|
// 填充产品列表数据 |
|
|
|
productList.value = props.record.map((item: ProductInfo) => { |
|
|
|
const product = createEmptyProduct(); |
|
|
|
Object.keys(product).forEach(key => { |
|
|
|
if (item[key as keyof ProductInfo] !== undefined) { |
|
|
|
// @ts-ignore |
|
|
|
product[key as keyof ProductInfo] = item[key as keyof ProductInfo]; |
|
|
|
} |
|
|
|
}); |
|
|
|
return product; |
|
|
|
}); |
|
|
|
|
|
|
|
// 如果没有产品,添加一个空白产品 |
|
|
|
if (productList.value.length === 0) { |
|
|
|
productList.value.push(createEmptyProduct()); |
|
|
|
} |
|
|
|
|
|
|
|
// 尝试加载PDF文件 |
|
|
|
try { |
|
|
|
if (props.record.id) { |
|
|
|
const response = await getPdfFile(props.record.taskId); |
|
|
|
if (props.record.length > 0 && props.record[0].taskId) { |
|
|
|
// @ts-ignore |
|
|
|
const response = await getPdfFile(props.record[0].taskId); |
|
|
|
if (response instanceof Blob) { |
|
|
|
pdfUrl.value = URL.createObjectURL(response); |
|
|
|
|
|
|
@ -151,23 +190,51 @@ |
|
|
|
} catch (error) { |
|
|
|
console.error('加载PDF文件失败:', error); |
|
|
|
} |
|
|
|
} else { |
|
|
|
// 如果没有记录,添加一个空白产品 |
|
|
|
productList.value.push(createEmptyProduct()); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 提交表单 |
|
|
|
const handleSubmit = async () => { |
|
|
|
// 添加产品 |
|
|
|
const handleAddProduct = () => { |
|
|
|
productList.value.push(createEmptyProduct()); |
|
|
|
}; |
|
|
|
|
|
|
|
// 删除产品 |
|
|
|
const handleRemoveProduct = (index: number) => { |
|
|
|
productList.value.splice(index, 1); |
|
|
|
}; |
|
|
|
|
|
|
|
// 提交单个产品 |
|
|
|
const handleSubmitSingle = async (index: number) => { |
|
|
|
try { |
|
|
|
await formRef.value.validate(); |
|
|
|
if (formRefs.value[index]) { |
|
|
|
await formRefs.value[index].validate(); |
|
|
|
} |
|
|
|
|
|
|
|
loading.value = true; |
|
|
|
|
|
|
|
// 调用API保存数据 |
|
|
|
const res = await updateContractualProductInfo(formData); |
|
|
|
// 获取当前产品 |
|
|
|
const currentProduct = productList.value[index]; |
|
|
|
|
|
|
|
// 根据产品是否有id决定调用哪个API |
|
|
|
let res; |
|
|
|
if (currentProduct.id) { |
|
|
|
// 已有产品,调用更新接口 |
|
|
|
res = await updateContractualProductInfo(currentProduct); |
|
|
|
} else { |
|
|
|
// 新增产品,调用新增接口 |
|
|
|
res = await addContractualProductInfo(currentProduct); |
|
|
|
// 如果新增成功,更新产品id |
|
|
|
if (res.code === 200 && res.data) { |
|
|
|
currentProduct.id = res.data.id; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (res.code === 200) { |
|
|
|
ElMessage.success('保存成功'); |
|
|
|
emit('success', formData); |
|
|
|
handleClose(); |
|
|
|
ElMessage.success(`产品 #${index + 1} ${currentProduct.id ? '更新' : '新增'}成功`); |
|
|
|
emit('success', currentProduct); |
|
|
|
} else { |
|
|
|
ElMessage.error(res.msg || '保存失败'); |
|
|
|
} |
|
|
@ -186,56 +253,84 @@ |
|
|
|
</script> |
|
|
|
|
|
|
|
<style scoped> |
|
|
|
/* 头部样式 */ |
|
|
|
.modal-header { |
|
|
|
overflow: hidden; |
|
|
|
.container { |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
height: 100%; |
|
|
|
} |
|
|
|
|
|
|
|
.header { |
|
|
|
display: flex; |
|
|
|
justify-content: space-between; |
|
|
|
align-items: center; |
|
|
|
padding: 16px 24px; |
|
|
|
background-color: #f5f7fa; |
|
|
|
border-bottom: 1px solid #e4e7ed; |
|
|
|
} |
|
|
|
|
|
|
|
.modal-title { |
|
|
|
float: left; |
|
|
|
.title { |
|
|
|
margin: 0; |
|
|
|
font-size: 18px; |
|
|
|
color: #303133; |
|
|
|
} |
|
|
|
|
|
|
|
.modal-actions { |
|
|
|
float: right; |
|
|
|
.actions { |
|
|
|
display: flex; |
|
|
|
gap: 12px; |
|
|
|
} |
|
|
|
|
|
|
|
.modal-actions .el-button { |
|
|
|
margin-left: 12px; |
|
|
|
.content { |
|
|
|
display: flex; |
|
|
|
flex: 1; |
|
|
|
overflow: hidden; |
|
|
|
} |
|
|
|
|
|
|
|
/* 确保embed标签占满整个容器 */ |
|
|
|
.pdf-embed { |
|
|
|
display: block; |
|
|
|
height: calc(100vh - 70px) !important; /* 减去头部的高度 */ |
|
|
|
border: none; |
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
|
|
|
.pdf-area { |
|
|
|
width: 50%; |
|
|
|
height: 100%; |
|
|
|
border-right: 1px solid #e4e7ed; |
|
|
|
overflow: hidden; |
|
|
|
} |
|
|
|
|
|
|
|
/* 使网格容器填充整个可用空间 */ |
|
|
|
.grid { |
|
|
|
height: calc(100vh - 70px); /* 减去头部的高度 */ |
|
|
|
overflow: hidden; |
|
|
|
.pdf-area embed { |
|
|
|
width: 100%; |
|
|
|
height: 100%; |
|
|
|
border: none; |
|
|
|
} |
|
|
|
|
|
|
|
/* 确保两列都有适当的滚动行为 */ |
|
|
|
.col-span-1 { |
|
|
|
.form-area { |
|
|
|
width: 50%; |
|
|
|
height: 100%; |
|
|
|
overflow-y: auto; |
|
|
|
padding: 16px; |
|
|
|
} |
|
|
|
|
|
|
|
/* 表单样式 */ |
|
|
|
.edit-form { |
|
|
|
padding: 20px; |
|
|
|
.product-item { |
|
|
|
margin-bottom: 20px; |
|
|
|
background-color: #f9fafc; |
|
|
|
border-radius: 4px; |
|
|
|
padding: 16px; |
|
|
|
} |
|
|
|
|
|
|
|
/* 全局高度 */ |
|
|
|
.h-full { |
|
|
|
height: 100%; |
|
|
|
.product-header { |
|
|
|
display: flex; |
|
|
|
justify-content: space-between; |
|
|
|
align-items: center; |
|
|
|
margin-bottom: 16px; |
|
|
|
} |
|
|
|
|
|
|
|
.product-header h3 { |
|
|
|
margin: 0; |
|
|
|
font-size: 16px; |
|
|
|
font-weight: 500; |
|
|
|
} |
|
|
|
|
|
|
|
.product-actions { |
|
|
|
display: flex; |
|
|
|
gap: 8px; |
|
|
|
} |
|
|
|
|
|
|
|
.edit-form { |
|
|
|
padding: 8px 0; |
|
|
|
} |
|
|
|
</style> |