|
|
|
<template>
|
|
|
|
<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="content">
|
|
|
|
<!-- 左侧PDF显示区域 -->
|
|
|
|
<div class="pdf-area">
|
|
|
|
<embed :src="pdfUrl" type="application/pdf" width="100%" height="100%" />
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 右侧编辑表单区域 -->
|
|
|
|
<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, computed } from 'vue';
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
|
|
|
import { updateContractualProductInfo, addContractualProductInfo, delContractualProductInfo } 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: Array as () => ProductInfo[],
|
|
|
|
required: true,
|
|
|
|
default: () => []
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// 定义Emits
|
|
|
|
const emit = defineEmits(['close', 'success']);
|
|
|
|
|
|
|
|
// 创建空白产品模板
|
|
|
|
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(productList, () => {
|
|
|
|
productList.value.forEach(product => {
|
|
|
|
product.totalPrice = product.unitPrice * product.quantity;
|
|
|
|
});
|
|
|
|
}, { deep: true });
|
|
|
|
|
|
|
|
// 初始化数据
|
|
|
|
onMounted(async () => {
|
|
|
|
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.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);
|
|
|
|
|
|
|
|
// 文件加载完成后自动适应宽度
|
|
|
|
nextTick(() => {
|
|
|
|
setTimeout(() => {
|
|
|
|
// 如果需要特殊处理PDF显示可以在这里添加
|
|
|
|
}, 500);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
console.error('获取PDF文件失败:返回的不是Blob对象');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error('加载PDF文件失败:', error);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// 如果没有记录,添加一个空白产品
|
|
|
|
productList.value.push(createEmptyProduct());
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// 添加产品
|
|
|
|
const handleAddProduct = () => {
|
|
|
|
productList.value.push(createEmptyProduct());
|
|
|
|
};
|
|
|
|
|
|
|
|
// 删除产品
|
|
|
|
const handleRemoveProduct = async (index: number) => {
|
|
|
|
const currentProduct = productList.value[index];
|
|
|
|
|
|
|
|
// 如果产品已有ID(已保存到数据库),则调用删除API
|
|
|
|
if (currentProduct.id) {
|
|
|
|
try {
|
|
|
|
// 添加用户确认
|
|
|
|
await ElMessageBox.confirm('确定要删除该产品记录吗?', '提示', {
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
type: 'warning'
|
|
|
|
});
|
|
|
|
|
|
|
|
loading.value = true;
|
|
|
|
const res = await delContractualProductInfo(currentProduct.id);
|
|
|
|
|
|
|
|
if (res.code === 200) {
|
|
|
|
ElMessage.success('删除成功');
|
|
|
|
productList.value.splice(index, 1);
|
|
|
|
emit('success', null); // 通知父组件刷新数据
|
|
|
|
} else {
|
|
|
|
ElMessage.error(res.msg || '删除失败');
|
|
|
|
}
|
|
|
|
} catch (error: any) {
|
|
|
|
if (error !== 'cancel') {
|
|
|
|
console.error('删除失败', error);
|
|
|
|
ElMessage.error('删除失败');
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
loading.value = false;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// 如果产品没有ID(未保存到数据库),直接从列表中移除
|
|
|
|
productList.value.splice(index, 1);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// 提交单个产品
|
|
|
|
const handleSubmitSingle = async (index: number) => {
|
|
|
|
try {
|
|
|
|
if (formRefs.value[index]) {
|
|
|
|
await formRefs.value[index].validate();
|
|
|
|
}
|
|
|
|
|
|
|
|
loading.value = true;
|
|
|
|
|
|
|
|
// 获取当前产品
|
|
|
|
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(`产品 #${index + 1} ${currentProduct.id ? '更新' : '新增'}成功`);
|
|
|
|
emit('success', currentProduct);
|
|
|
|
} else {
|
|
|
|
ElMessage.error(res.msg || '保存失败');
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error('提交失败', error);
|
|
|
|
ElMessage.error('表单验证失败,请检查输入');
|
|
|
|
} finally {
|
|
|
|
loading.value = false;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// 关闭页面
|
|
|
|
const handleClose = () => {
|
|
|
|
emit('close');
|
|
|
|
};
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
.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;
|
|
|
|
}
|
|
|
|
|
|
|
|
.title {
|
|
|
|
margin: 0;
|
|
|
|
font-size: 18px;
|
|
|
|
color: #303133;
|
|
|
|
}
|
|
|
|
|
|
|
|
.actions {
|
|
|
|
display: flex;
|
|
|
|
gap: 12px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.content {
|
|
|
|
display: flex;
|
|
|
|
flex: 1;
|
|
|
|
overflow: hidden;
|
|
|
|
}
|
|
|
|
|
|
|
|
.pdf-area {
|
|
|
|
width: 50%;
|
|
|
|
height: 100%;
|
|
|
|
border-right: 1px solid #e4e7ed;
|
|
|
|
overflow: hidden;
|
|
|
|
}
|
|
|
|
|
|
|
|
.pdf-area embed {
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
border: none;
|
|
|
|
}
|
|
|
|
|
|
|
|
.form-area {
|
|
|
|
width: 50%;
|
|
|
|
height: 100%;
|
|
|
|
overflow-y: auto;
|
|
|
|
padding: 16px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.product-item {
|
|
|
|
margin-bottom: 20px;
|
|
|
|
background-color: #f9fafc;
|
|
|
|
border-radius: 4px;
|
|
|
|
padding: 16px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.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>
|