You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

938 lines
39 KiB
Vue

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="p-2">
<el-card shadow="never">
<approvalButton
@submitForm="submitForm"
@approvalVerifyOpen="approvalVerifyOpen"
@handleApprovalRecord="handleApprovalRecord"
:buttonLoading="buttonLoading"
:id="form.quoteId as any"
:status="form.flowStatus as any"
:pageType="routeParams.type"
:mode="false"
/>
</el-card>
<el-card shadow="never">
<!-- 统一表单包裹所有分区便于整体验证 -->
<el-form ref="quoteFormRef" :model="form" :rules="rules" label-width="120px" :disabled="isView">
<el-divider content-position="left">基本信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="报价单编号" prop="quoteCode">
<el-input v-model="form.quoteCode" placeholder="自动生成">
<template #append>
<el-button type="primary" @click="generateQuoteCode" :disabled="isView || isCodeGenerated">生成报价单编号</el-button>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="报价单名称" prop="quoteName">
<el-input v-model="form.quoteName" placeholder="请输入报价单名称" />
</el-form-item>
</el-col>
<!-- 与截图不符的字段按要求暂时注释,保留原位置方便后续恢复 -->
<!--
<el-col :span="12">
<el-form-item label="报价大类" prop="quoteCategory">
<el-select v-model="form.quoteCategory" placeholder="请选择报价大类">
<el-option v-for="dict in quote_category" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="报价类型" prop="quoteType">
<el-select v-model="form.quoteType" placeholder="请选择报价类型">
<el-option v-for="dict in contract_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="业务方向" prop="businessDirection">
<el-select v-model="form.businessDirection" placeholder="请选择业务方向">
<el-option v-for="dict in business_direction" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
-->
<el-col :span="12">
<el-form-item label="报价日期" prop="quoteDate">
<el-date-picker clearable v-model="form.quoteDate" type="date" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择报价日期" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="有效期起" prop="validFrom">
<el-date-picker clearable v-model="form.validFrom" type="date" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择有效期起" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="有效期止" prop="validTo">
<el-date-picker clearable v-model="form.validTo" type="date" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择有效期止" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="交货期(天)" prop="deliveryPeriod">
<!-- 使用数字输入框,禁用随表单统一控制 -->
<el-input-number v-model="form.deliveryPeriod" :min="0" :precision="0" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="交货方式" prop="deliveryMethod">
<el-input v-model="form.deliveryMethod" placeholder="请输入交货方式" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="付款方式" prop="paymentMethod">
<el-select v-model="form.paymentMethod" placeholder="请选择付款方式">
<el-option v-for="dict in payment_method" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="币种" prop="currencyType">
<el-select v-model="form.currencyType" placeholder="请选择币种">
<el-option v-for="dict in currency_type" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="含税信息" prop="taxIncludedInfo">
<el-input v-model="form.taxIncludedInfo" placeholder="如含13%增值税" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="税率(%)" prop="taxRate">
<!-- 使用数字输入框两位小数步长0.01 -->
<el-input-number v-model="form.taxRate" :min="0" :precision="2" :step="0.01" style="width: 100%" />
</el-form-item>
</el-col>
<!-- 原客户方信息块已迁移到“客户方信息”分区,保留以便后续恢复 -->
<el-col :span="12" v-if="false">
<el-form-item label="客户联系人" prop="customerContactId">
<el-select v-model="form.customerContactId" filterable placeholder="请选择客户联系人" @change="onCustomerContactChanged">
<el-option
v-for="c in customerContactList"
:key="c.contactId"
:label="c.contactName + ' - ' + (c.phoneNumber || '')"
:value="c.contactId"
/>
</el-select>
</el-form-item>
<!-- 客户方联系电话与邮箱(随选择带出,可编辑) -->
<el-form-item label="客户联系电话" prop="customerContactPhone">
<el-input v-model="form.customerContactPhone" placeholder="客户联系电话" />
</el-form-item>
<el-form-item label="客户邮箱" prop="customerContactEmail">
<el-input v-model="form.customerContactEmail" placeholder="客户邮箱" />
</el-form-item>
</el-col>
<!-- 原供货方信息块已迁移到“供货方信息”分区,保留以便后续恢复 -->
<el-col :span="12" v-if="false">
<el-form-item label="供货方联系人" prop="supplierContactId">
<el-select v-model="form.supplierContactId" filterable placeholder="请选择供货方联系人" @change="onSupplierChanged">
<el-option v-for="c in supplierList" :key="c.supplierId" :label="c.supplierName" :value="c.supplierId" />
</el-select>
</el-form-item>
<!-- 供货方联系电话与邮箱(随选择带出,可编辑) -->
<el-form-item label="供货方联系电话" prop="supplierContactPhone">
<el-input v-model="form.supplierContactPhone" placeholder="供货方联系电话" />
</el-form-item>
<el-form-item label="供货方邮箱" prop="supplierContactEmail">
<el-input v-model="form.supplierContactEmail" placeholder="供货方邮箱" />
</el-form-item>
</el-col>
<!-- 原备注块已迁移到“备注”分区,保留以便后续恢复 -->
<el-col :span="12" v-if="false">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-col>
<!-- 原附件上传块已迁移到“附件”分区,保留以便后续恢复 -->
<el-col :span="12" v-if="false">
<el-form-item label="附件上传">
<el-button type="primary" plain icon="Upload" @click="handleFile">上传附件</el-button>
<div style="margin-top: 8px; color: #999">支持格式:.rar .zip .doc .docx .pdf单个文件不超过20MB</div>
</el-form-item>
</el-col>
</el-row>
<!-- 报价物料编辑对话框 -->
<el-dialog :title="materialDialog.title" v-model="materialDialog.visible" width="800px" append-to-body>
<el-form ref="materialFormRef" :model="materialForm" :rules="materialRules" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="标准物料标识" prop="materialFlag">
<el-radio-group v-model="materialForm.materialFlag">
<el-radio v-for="dict in material_flag" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="产品名称" prop="productName">
<el-input v-model="materialForm.productName" placeholder="请输入产品名称">
<template #suffix>
<el-icon style="cursor: pointer" v-if="materialForm.materialFlag === '1'" @click="openSaleMaterialSelect">
<Search />
</el-icon>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="规格描述" prop="specificationDescription">
<el-input v-model="materialForm.specificationDescription" placeholder="请输入规格描述" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="数量" prop="amount">
<el-input-number
v-model="materialForm.amount"
placeholder="请输入数量"
style="width: 100%"
@change="calculateSubtotal"
:precision="2"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="物料单位" prop="unitId">
<el-select v-model="materialForm.unitId" placeholder="请选择物料单位">
<el-option v-for="item in unitInfoList" :key="item.unitId" :label="item.unitName" :value="item.unitId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="税率(%)" prop="taxRate">
<el-select v-model="materialForm.taxRate" placeholder="请选择税率" style="width: 100%" @change="calculateBeforePrice">
<el-option label="6" :value="6" />
<el-option label="13" :value="13" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="未税单价" prop="beforePrice">
<el-input-number
v-model="materialForm.beforePrice"
placeholder="自动计算"
style="width: 100%"
:precision="2"
:controls="false"
disabled
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="含税单价" prop="includingPrice">
<el-input-number
v-model="materialForm.includingPrice"
placeholder="请输入含税单价"
style="width: 100%"
:precision="2"
@change="calculateBeforePrice"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="小计" prop="subtotal">
<el-input-number
v-model="materialForm.subtotal"
placeholder="自动计算"
style="width: 100%"
readonly
:precision="2"
:controls="false"
disabled
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="materialForm.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitMaterialForm">确 定</el-button>
<el-button @click="cancelMaterial">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 销售物料选择 -->
<SaleMaterialSelect ref="saleMaterialSelectRef" :multiple="false" @confirm-call-back="saleMaterialSelectCallBack" />
<!-- 对齐合同编辑页:单卡片内分区展示 -->
<el-divider content-position="left">客户方信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="客户联系人" prop="customerContactId">
<el-select v-model="form.customerContactId" filterable placeholder="请选择客户联系人" @change="onCustomerContactChanged">
<el-option
v-for="c in customerContactList"
:key="c.contactId"
:label="c.contactName + ' - ' + (c.phoneNumber || '')"
:value="c.contactId"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户联系电话" prop="customerContactPhone">
<el-input v-model="form.customerContactPhone" placeholder="客户联系电话" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户邮箱" prop="customerContactEmail">
<el-input v-model="form.customerContactEmail" placeholder="客户邮箱" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">供货方信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="计划标识" prop="supplierPlanFlag">
<el-radio-group v-model="supplierPlanFlag" @change="onSupplierPlanFlagChanged">
<el-radio value="1">计划内</el-radio>
<el-radio value="2">计划外</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" v-if="supplierPlanFlag === '1'">
<el-form-item label="供应商" prop="supplierContactId">
<el-select v-model="form.supplierContactId" filterable placeholder="请选择供应商" @change="onSupplierChanged">
<el-option v-for="s in supplierList" :key="s.supplierId" :label="s.supplierName" :value="s.supplierId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="供货方联系人" prop="supplierContactName">
<el-input v-model="form.supplierContactName" placeholder="供货方联系人" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="供货方联系电话" prop="supplierContactPhone">
<el-input v-model="form.supplierContactPhone" placeholder="供货方联系电话" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="供货方邮箱" prop="supplierContactEmail">
<el-input v-model="form.supplierContactEmail" placeholder="供货方邮箱" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">附件与备注</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="附件">
<el-button type="primary" plain icon="Upload" @click="handleFile" :disabled="isView">上传附件</el-button>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- 报价物料管理仅展示业务字段不展示技术ID -->
<el-divider content-position="left">报价明细</el-divider>
<el-row :gutter="10" class="mb-3" v-if="!isView">
<el-col :span="1.5">
<el-button type="primary" icon="Plus" @click="handleAddMaterial">新增物料</el-button>
</el-col>
</el-row>
<el-table :data="materialRows" border show-summary :summary-method="getSummary">
<el-table-column label="产品名称" align="center" prop="productName" min-width="160" />
<el-table-column label="规格描述" align="center" prop="specificationDescription" min-width="160" />
<el-table-column label="物料编号" align="center" prop="materialCode" width="140" />
<el-table-column label="物料名称" align="center" prop="materialName" width="160" />
<el-table-column label="销售物料名称" align="center" prop="saleMaterialName" width="160" />
<el-table-column label="数量" align="center" prop="amount" width="120">
<template #default="scope">
{{ scope.row.amount ? Number(scope.row.amount).toFixed(2) : '0.00' }}
</template>
</el-table-column>
<el-table-column label="单位" align="center" prop="unitName" width="120" />
<el-table-column label="未税单价" align="center" prop="beforePrice" width="140">
<template #default="scope">
{{ scope.row.beforePrice ? Number(scope.row.beforePrice).toFixed(2) : '0.00' }}
</template>
</el-table-column>
<el-table-column label="税率(%)" align="center" prop="taxRate" width="120">
<template #default="scope">
{{ scope.row.taxRate ? Number(scope.row.taxRate).toFixed(2) : '0.00' }}
</template>
</el-table-column>
<el-table-column label="含税单价" align="center" prop="includingPrice" width="140">
<template #default="scope">
{{ scope.row.includingPrice ? Number(scope.row.includingPrice).toFixed(2) : '0.00' }}
</template>
</el-table-column>
<el-table-column label="小计" align="center" prop="subtotal" width="140">
<template #default="scope">
{{ scope.row.subtotal ? Number(scope.row.subtotal).toFixed(2) : '0.00' }}
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" min-width="140" />
<el-table-column label="操作" align="center" fixed="right" width="150" v-if="!isView">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleEditMaterial(scope.row)">编辑</el-button>
<el-button link type="danger" icon="Delete" @click="handleDeleteMaterial(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div style="margin-top: 16px; padding-top: 12px; border-top: 1px solid #ebeef5; text-align: right">
<span style="margin-right: 20px; font-weight: bold; font-size: 14px">
含税总价:<span style="color: #f56c6c">{{ totalIncludingTax.toFixed(2) }}</span>
</span>
</div>
</el-card>
<ApprovalRecord ref="approvalRecordRef" />
<SubmitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
<!-- 附件上传对话框 -->
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
<el-form label-width="80px">
<el-form-item label="文件名">
<fileUpload v-if="type === 0" v-model="ossFileModel" />
<imageUpload v-if="type === 1" v-model="ossFileModel" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitOss">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</template>
</el-dialog>
</div>

</template>
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
import { addCrmQuoteInfo, getCrmQuoteInfo, quoteSubmitAndFlowStart, updateCrmQuoteInfo } from '@/api/oa/crm/crmQuoteInfo';
import { CrmQuoteInfoForm } from '@/api/oa/crm/crmQuoteInfo/types';
import type { CrmQuoteMaterialForm } from '@/api/oa/crm/crmQuoteMaterial/types';
import { getCrmCustomerContactList } from '@/api/oa/crm/customerContact';
import { getCrmSupplierInfoList } from '@/api/oa/crm/crmSupplierInfo';
import { getRuleGenerateCode } from '@/api/system/codeRule';
import { getBaseUnitInfoList } from '@/api/oa/base/unitInfo';
import SaleMaterialSelect from '@/components/SaleMaterialSelect/index.vue';
import ApprovalButton from '@/components/Process/approvalButton.vue';
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
import SubmitVerify from '@/components/Process/submitVerify.vue';
import { FlowCodeEnum } from '@/enums/OAEnum';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { business_direction, currency_type, quote_category, contract_type, material_flag, wf_business_status, payment_method } = toRefs<any>(
proxy?.useDict('business_direction', 'currency_type', 'quote_category', 'contract_type', 'material_flag', 'wf_business_status', 'payment_method')
);
const router = useRouter();
const route = useRoute();
// 统一路由参数管理(与 projectInfo/projectPurchase 保持一致)
const routeParams = ref<Record<string, any>>({});
// 查看/审批模式根据路由type控制禁用
const isView = computed(() => routeParams.value.type === 'view' || routeParams.value.type === 'approval');
// 流程变量(传递给 submitVerify 组件)
const taskVariables = ref<Record<string, any>>({});
const buttonLoading = ref(false);
const quoteFormRef = ref<ElFormInstance>();
// 编号生成状态(生成后禁用按钮)
const isCodeGenerated = ref(false);
const form = reactive<CrmQuoteInfoForm>({
quoteId: undefined,
quoteCode: undefined,
quoteName: undefined,
quoteCategory: undefined,
quoteType: undefined,
businessDirection: undefined,
quoteDate: undefined,
validFrom: undefined,
validTo: undefined,
deliveryPeriod: undefined,
deliveryMethod: undefined,
paymentMethod: undefined,
currencyType: undefined,
taxIncludedInfo: undefined,
taxRate: undefined,
customerContactId: undefined,
customerContactName: undefined,
customerContactPhone: undefined,
customerContactEmail: undefined,
supplierContactId: undefined,
supplierContactName: undefined,
supplierContactPhone: undefined,
supplierContactEmail: undefined,
remark: undefined
});
const rules = {
quoteName: [{ required: true, message: '报价单名称不能为空', trigger: 'blur' }],
customerContactId: [{ required: true, message: '请选择客户联系人', trigger: 'change' }]
};
// 下拉数据
const customerContactList = ref<any[]>([]);
const supplierList = ref<any[]>([]);
// 供应商计划标识1计划内2计划外
const supplierPlanFlag = ref<string>('1');
// 单位下拉数据
const unitInfoList = ref<any[]>([]);
const getUnitInfoListSelect = async () => {
const res = await getBaseUnitInfoList(null);
unitInfoList.value = res.data || [];
};
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
// 物料编辑弹窗与表单
const materialDialog = reactive({ visible: false, title: '' });
const materialFormRef = ref<ElFormInstance>();
const initMaterialFormData: CrmQuoteMaterialForm & { materialFlag?: string } = {
quoteMaterialId: undefined,
materialFlag: '2', // 1标准物料 2非标物料
quoteId: undefined,
productName: undefined,
specificationDescription: undefined,
materialId: undefined,
relationMaterialId: undefined,
materialCode: undefined,
materialName: undefined,
saleMaterialName: undefined,
amount: undefined,
unitId: undefined,
unitName: undefined,
beforePrice: undefined,
taxRate: 13,
includingPrice: undefined,
subtotal: undefined,
remark: undefined,
activeFlag: '1'
};
const materialForm = ref<CrmQuoteMaterialForm & { materialFlag?: string }>({ ...initMaterialFormData });
// 当前编辑的物料行索引null 表示新增
const currentMaterialIndex = ref<number | null>(null);
const materialRules = {
productName: [{ required: true, message: '产品名称不能为空', trigger: 'blur' }],
amount: [{ required: true, message: '数量不能为空', trigger: 'blur' }],
taxRate: [{ required: true, message: '请选择税率', trigger: 'change' }]
};
const saleMaterialSelectRef = ref<InstanceType<typeof SaleMaterialSelect>>();
const openSaleMaterialSelect = () => saleMaterialSelectRef.value?.open();
const saleMaterialSelectCallBack = (data: any) => {
const list = data || [];
if (list.length) {
const m = list[0];
materialForm.value.materialId = m.materialId;
materialForm.value.relationMaterialId = m.relationMaterialId;
materialForm.value.materialCode = m.materialCode;
materialForm.value.materialName = m.materialName;
materialForm.value.saleMaterialName = m.saleMaterialName;
materialForm.value.productName = m.saleMaterialName || m.materialName;
if (m.unitName) {
materialForm.value.unitName = m.unitName;
}
}
};
const resetMaterialForm = () => {
materialForm.value = { ...initMaterialFormData };
materialFormRef.value?.resetFields();
currentMaterialIndex.value = null;
};
const cancelMaterial = () => {
resetMaterialForm();
materialDialog.visible = false;
};
const calculateBeforePrice = () => {
const tax = Number(materialForm.value.taxRate);
const including = Number(materialForm.value.includingPrice);
if (!isNaN(tax) && !isNaN(including)) {
const divisor = 1 + tax / 100;
if (divisor > 0) {
materialForm.value.beforePrice = Number((including / divisor).toFixed(2));
}
}
calculateSubtotal();
};
const calculateSubtotal = () => {
const amount = Number(materialForm.value.amount);
const including = Number(materialForm.value.includingPrice);
if (!isNaN(amount) && !isNaN(including)) {
materialForm.value.subtotal = Number((amount * including).toFixed(2));
}
};
const handleAddMaterial = () => {
resetMaterialForm();
materialForm.value.quoteId = form.quoteId as any;
currentMaterialIndex.value = null;
materialDialog.visible = true;
materialDialog.title = '新增报价物料';
};
const handleEditMaterial = (row: CrmQuoteMaterialForm & { materialFlag?: string }) => {
resetMaterialForm();
// 优先使用已有 materialFlag没有则根据是否选择物料ID推导标准/非标
const flag = (row as any).materialFlag ?? ((row as any).materialId ? '1' : '2');
const idx = materialRows.value.indexOf(row as any);
currentMaterialIndex.value = idx !== -1 ? idx : null;
materialForm.value = { ...(row as any), materialFlag: flag };
materialDialog.visible = true;
materialDialog.title = '编辑报价物料';
};
const handleDeleteMaterial = async (row: CrmQuoteMaterialForm) => {
await proxy?.$modal.confirm('是否确认删除该报价物料?');
const idx = materialRows.value.indexOf(row as any);
if (idx !== -1) {
materialRows.value.splice(idx, 1);
}
proxy?.$modal.msgSuccess('删除成功');
};
const submitMaterialForm = () => {
materialFormRef.value?.validate((valid: boolean) => {
if (!valid) return;
if (currentMaterialIndex.value !== null && currentMaterialIndex.value >= 0) {
materialRows.value[currentMaterialIndex.value] = { ...materialForm.value } as any;
} else {
const newItem = { ...materialForm.value } as any;
materialRows.value.push(newItem);
}
proxy?.$modal.msgSuccess('操作成功');
materialDialog.visible = false;
});
};
// 物料行
const materialRows = ref<CrmQuoteMaterialForm[]>([]);
const totalIncludingTax = computed(() => {
return materialRows.value.reduce((sum, r) => sum + Number(r.subtotal || 0), 0);
});
// 表格合计行:仅统计小计列
const getSummary = (param: any) => {
const { columns, data } = param;
const sums: string[] = [];
columns.forEach((column: any, index: number) => {
if (index === 0) {
sums[index] = '合计';
return;
}
if (column.property === 'subtotal') {
const total = data.reduce((sum: number, row: any) => sum + Number(row.subtotal || 0), 0);
sums[index] = total.toFixed(2);
} else {
sums[index] = '';
}
});
return sums;
};
const addMaterialRow = () => {
// 默认支持非标:预留自填名称与规格;单位可手填
materialRows.value.push({
materialId: undefined,
materialCode: '',
materialName: '',
saleMaterialName: '',
unitId: undefined,
unitName: '',
productName: '',
specificationDescription: '',
amount: 0,
beforePrice: 0,
taxRate: Number(form.taxRate || 0),
includingPrice: 0,
subtotal: 0,
remark: ''
} as CrmQuoteMaterialForm);
};
const removeRow = (idx: number) => materialRows.value.splice(idx, 1);
// 工具:数值化与保留两位小数
const toNum = (v: any) => (v === '' || v === null || v === undefined ? undefined : Number.isFinite(Number(v)) ? Number(v) : undefined);
const round2 = (n: number) => Math.round(n * 100) / 100;
// 计算器
const calcIncluding = (before: number, rate: number) => round2(before * (1 + rate / 100));
const calcBefore = (including: number, rate: number) => round2(including / (1 + rate / 100));
const calcRate = (including: number, before: number) => round2((including / before - 1) * 100);
// 统一小计:始终以含税单价×数量;含税缺失则以未税+税率推导
const updateSubtotal = (row: any) => {
const amount = toNum(row.amount) ?? 0;
const before = toNum(row.beforePrice);
const including = toNum(row.includingPrice);
const rate = toNum(row.taxRate) ?? toNum(form.taxRate);
let inc = including;
if (inc === undefined && before !== undefined && rate !== undefined) {
inc = calcIncluding(before, rate);
row.includingPrice = inc; // 推导同时回填,保持一致
}
row.subtotal = inc !== undefined ? round2((inc as number) * amount) : 0;
};
// 基于“最近编辑字段”的确定性互算:任意两项给出,自动算第三项
const onAmountChange = (row: any, val: number) => {
row.amount = val;
updateSubtotal(row);
};
const onBeforePriceChange = (row: any, val: number) => {
row.beforePrice = val;
const rate = toNum(row.taxRate) ?? toNum(form.taxRate);
const including = toNum(row.includingPrice);
if (rate !== undefined) {
row.includingPrice = calcIncluding(val, rate as number);
} else if (including !== undefined) {
row.taxRate = calcRate(including as number, val);
}
updateSubtotal(row);
};
const onTaxRateChange = (row: any, val: number) => {
row.taxRate = val;
const before = toNum(row.beforePrice);
const including = toNum(row.includingPrice);
if (before !== undefined) {
row.includingPrice = calcIncluding(before as number, val);
} else if (including !== undefined) {
row.beforePrice = calcBefore(including as number, val);
}
updateSubtotal(row);
};
const onIncludingPriceChange = (row: any, val: number) => {
row.includingPrice = val;
const rate = toNum(row.taxRate) ?? toNum(form.taxRate);
const before = toNum(row.beforePrice);
if (rate !== undefined) {
row.beforePrice = calcBefore(val, rate as number);
} else if (before !== undefined) {
row.taxRate = calcRate(val, before as number);
}
updateSubtotal(row);
};
// 选中联系人后,自动带出姓名、电话、邮箱(可编辑,不影响再次选择)
const onCustomerContactChanged = (id: any) => {
const c = customerContactList.value.find((x: any) => x.contactId === id);
form.customerContactName = c?.contactName || '';
form.customerContactPhone = c?.phoneNumber || '';
form.customerContactEmail = c?.email || '';
};
// 供应商计划标识变化处理
const onSupplierPlanFlagChanged = (flag: string) => {
supplierPlanFlag.value = flag;
// 切换计划标识时清空供应商选择
form.supplierContactId = undefined;
form.supplierContactName = '';
form.supplierContactPhone = '';
form.supplierContactEmail = '';
};
// 供应商选择变化处理(计划内供应商)
const onSupplierChanged = (supplierId: any) => {
const supplier = supplierList.value.find((x: any) => x.supplierId === supplierId);
if (supplier) {
// 从供应商信息中带出联系人、电话、邮箱(可编辑)
form.supplierContactName = supplier.contactPerson || '';
form.supplierContactPhone = supplier.contactPhone || supplier.contactMobile || '';
form.supplierContactEmail = supplier.contactEmail || '';
}
};
// 生成报价单编号codeRuleCode=1004
const generateQuoteCode = async () => {
if (isCodeGenerated.value) return;
try {
const params = { codeRuleCode: '1004' } as any;
const res = await getRuleGenerateCode(params);
form.quoteCode = res.msg;
isCodeGenerated.value = true;
proxy?.$modal.msgSuccess('报价单编号生成成功');
} catch (error) {
proxy?.$modal.msgError('报价单编号生成失败');
}
};
// 附件上传对话框UI占位复用通用上传组件
const type = ref(0);
const dialog = reactive({ visible: false, title: '' });
const ossFileModel = ref<string | string[] | undefined>(undefined);
const handleFile = () => {
type.value = 0;
dialog.visible = true;
dialog.title = '上传报价附件';
};
const submitOss = () => {
dialog.visible = false;
proxy?.$modal.msgSuccess('附件已更新');
};
const cancel = () => {
dialog.visible = false;
};
const submitForm = (status: string, mode: boolean) => {
quoteFormRef.value?.validate(async (valid: boolean) => {
if (!valid) return;
buttonLoading.value = true;
try {
const payload: any = {
...form,
itemsBo: materialRows.value.map((r, idx) => ({
...r,
itemNo: idx + 1,
subtotal: Number(r.subtotal || 0)
}))
};
if (status === 'draft') {
// === 暂存 ===
payload.quoteStatus = '1';
payload.flowStatus = 'draft';
if (!form.quoteId) {
await addCrmQuoteInfo(payload);
} else {
await updateCrmQuoteInfo(payload);
}
proxy?.$modal.msgSuccess('暂存成功');
} else {
// === 提交审批 ===
payload.flowCode = FlowCodeEnum.QUOTE_CODE; //OACQ
payload.variables = {
quoteId: form.quoteId,
quoteName: form.quoteName,
quoteCode: form.quoteCode,
totalPrice: totalIncludingTax.value
};
payload.bizExt = {
businessTitle: '报价单审批',
businessCode: form.quoteCode
};
payload.quoteStatus = '2';
payload.flowStatus = 'waiting';
const res = await quoteSubmitAndFlowStart(payload);
if (res?.data) {
Object.assign(form, res.data as any);
}
proxy?.$modal.msgSuccess('提交成功');
}
// 统一的成功后行为(与 projectInfo 保持一致)
proxy?.$tab.closePage(route as any);
router.go(-1);
} finally {
buttonLoading.value = false;
}
});
};
const approvalVerifyOpen = async () => {
// 设置流程变量
taskVariables.value = {
quoteId: form.quoteId,
quoteName: form.quoteName,
quoteCode: form.quoteCode,
totalPrice: totalIncludingTax.value
};
const taskId = routeParams.value.taskId;
if (taskId) {
await submitVerifyRef.value?.openDialog(taskId);
}
};
const handleApprovalRecord = () => {
if (form.quoteId) {
approvalRecordRef.value?.init(form.quoteId as any);
}
};
/** 提交审批回调(与 projectInfo 保持一致) */
const submitCallback = async () => {
await proxy?.$tab.closePage(route as any);
router.go(-1);
};
onMounted(async () => {
// 初始化路由参数(与 projectInfo/projectPurchase 保持一致)
routeParams.value = { ...route.query };
// 下拉数据初始化
const contactRes = await getCrmCustomerContactList({});
customerContactList.value = contactRes.data || [];
// 加载供应商列表(计划内供应商)
const supplierRes = await getCrmSupplierInfoList({});
supplierList.value = supplierRes.data || [];
await getUnitInfoListSelect();
// 编辑场景
const id = route.query.id || route.params.id;
if (id) {
// 先回填主键,防止数据尚未加载完用户就点击保存而误走新增
// 前端保持 quoteId 为字符串,避免长整型精度丢失
form.quoteId = id as any;
const res = await getCrmQuoteInfo(id as any);
Object.assign(form, res.data);
// 编辑模式:已存在编号则禁用生成
isCodeGenerated.value = !!form.quoteCode;
// 根据已有联系人ID回填姓名、电话、邮箱若后端已返回则保持
if (!form.customerContactPhone || !form.customerContactEmail || !form.customerContactName) {
onCustomerContactChanged(form.customerContactId);
}
// 供应商信息回填:判断是否为计划内供应商
if (form.supplierContactId) {
const supplier = supplierList.value.find((x: any) => x.supplierId === form.supplierContactId);
if (supplier) {
// 计划内供应商
supplierPlanFlag.value = '1';
if (!form.supplierContactPhone || !form.supplierContactEmail || !form.supplierContactName) {
onSupplierChanged(form.supplierContactId);
}
} else {
// 计划外供应商
supplierPlanFlag.value = '2';
}
}
// 仅使用后端VO内联返回的 itemsVo前端不再单独请求子表接口
const inlineItems = (res.data as any)?.itemsVo;
if (inlineItems && inlineItems.length) {
// 后端 BigDecimal/字符串数值需在前端转为 number确保 el-input-number 与 toFixed 正常工作
materialRows.value = inlineItems.map((r: any) => ({
quoteMaterialId: r.quoteMaterialId,
quoteId: r.quoteId,
itemNo: r.itemNo,
materialId: r.materialId,
relationMaterialId: r.relationMaterialId,
materialCode: r.materialCode,
materialName: r.materialName,
saleMaterialName: r.saleMaterialName,
unitId: r.unitId,
unitName: r.unitName,
productName: r.productName,
specificationDescription: r.specificationDescription,
amount: Number(r.amount ?? 0),
beforePrice: Number(r.beforePrice ?? 0),
taxRate: Number(r.taxRate ?? form.taxRate ?? 0),
includingPrice: Number(r.includingPrice ?? 0),
subtotal: Number(r.subtotal ?? 0),
remark: r.remark
}));
} else {
materialRows.value = [];
}
}
// 新增模式:如已有编号则禁用生成按钮
if (!id) {
isCodeGenerated.value = !!form.quoteCode;
}
});
</script>
<style scoped>
/* 基础信息分区整体居中显示 */
.basic-center {
max-width: 860px;
margin: 0 auto;
}
</style>