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.

1133 lines
48 KiB
Vue

This file contains ambiguous Unicode 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">
<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="自动生成" disabled>
<!-- <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="请输入报价单名称" :disabled="isView" />
</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="请选择报价日期"
:disabled="isView"
/>
</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="请选择有效期起"
:disabled="isView"
/>
</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="请选择有效期止"
:disabled="isView"
/>
</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" :disabled="isView" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="质保期" prop="warrantyPeriod">
<el-input-number v-model="form.warrantyPeriod" :min="0" :precision="0" :disabled="isView" 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="请输入交货方式" :disabled="isView" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="付款方式" prop="paymentMethod">
<el-select v-model="form.paymentMethod" placeholder="请选择付款方式" :disabled="isView">
<el-option v-for="item in paymentMethodOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="付款明细" prop="paymentDetail">
<el-input
v-model="form.paymentDetail"
placeholder="请输入付款节点及付款比例预付款30%验收款70%等)"
:disabled="isView"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="币种" prop="currencyType">
<el-select v-model="form.currencyType" placeholder="请选择币种" :disabled="isView">
<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%增值税" :disabled="isView" />
</el-form-item>
</el-col>
<!-- <el-col :span="12">-->
<!-- <el-form-item label="税率(%)" prop="taxRate">-->
<!-- <el-select-->
<!-- v-model="form.taxRate"-->
<!-- placeholder="请选择或输入税率"-->
<!-- style="width: 100%"-->
<!-- :disabled="isView"-->
<!-- filterable-->
<!-- allow-create-->
<!-- default-first-option-->
<!-- >-->
<!-- <el-option label="0" :value="0"></el-option>-->
<!-- <el-option label="6" :value="6"></el-option>-->
<!-- <el-option label="9" :value="9"></el-option>-->
<!-- <el-option label="13" :value="13"></el-option>-->
<!-- </el-select>-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<el-col :span="12">
<el-form-item label="打印模板" prop="templateId">
<div class="flex gap-2 items-center" style="width: 100%">
<el-select v-model="form.templateId" placeholder="请选择报价单打印模板" :disabled="isView" filterable clearable style="flex: 1">
<el-option
v-for="item in printTemplateList"
:key="item.templateId"
:label="item.templateName + (item.version ? '-' + item.version : '')"
:value="item.templateId"
/>
</el-select>
<el-button
link
type="primary"
icon="Download"
:disabled="!form.templateId || !form.quoteId"
@click="handleQuoteTemplateDownload"
style="font-weight: 600"
v-hasPermi="['oa/crm:crmQuoteInfo:export']"
>
报价单查看
</el-button>
</div>
</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"
: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-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" @change="handleMaterialFlagChange">
<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="materialName" v-if="materialForm.materialFlag === '1'">
<el-input v-model="materialForm.materialName" placeholder="请点击右侧图标检索物料" readonly>
<template #suffix>
<el-icon style="cursor: pointer" @click="openSaleMaterialSelect">
<Search />
</el-icon>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="物料编号" prop="materialCode" v-if="materialForm.materialFlag === '1'">
<el-input v-model="materialForm.materialCode" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="销售物料名称" prop="saleMaterialName" v-if="materialForm.materialFlag === '1'">
<el-input v-model="materialForm.saleMaterialName" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="产品名称" prop="productName">
<el-input v-model="materialForm.productName" placeholder="请输入产品名称(合同显示)" @input="handleMaterialProductNameInput" />
</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="0" :value="0"></el-option>
<el-option label="6" :value="6"></el-option>
<el-option label="9" :value="9"></el-option>
<el-option label="13" :value="13"></el-option>
</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="customerId">
<el-select v-model="form.customerId" filterable placeholder="请选择客户名称" @change="onCustomerChanged" :disabled="isView">
<el-option v-for="c in customerList" :key="c.customerId" :label="c.customerName" :value="c.customerId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户联系人" prop="customerContactId">
<el-select
v-model="form.customerContactId"
filterable
clearable
placeholder="请选择客户联系人"
@change="onCustomerContactChanged"
:disabled="isView"
>
<el-option
v-for="c in filteredCustomerContactList"
:key="c.contactId"
:label="c.contactName"
: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="客户联系电话" :disabled="isView" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户邮箱" prop="customerContactEmail">
<el-input v-model="form.customerContactEmail" placeholder="客户邮箱" :disabled="isView" />
</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="supplierContactId">
<el-select v-model="form.supplierContactId" filterable placeholder="请选择供应商" @change="onSupplierChanged" :disabled="isView">
<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="供货方联系人" :disabled="isView" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="供货方联系电话" prop="supplierContactPhone">
<el-input v-model="form.supplierContactPhone" placeholder="供货方联系电话" :disabled="isView" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="供货方邮箱" prop="supplierContactEmail">
<el-input v-model="form.supplierContactEmail" placeholder="供货方邮箱" :disabled="isView" />
</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="ossId">
<FileUpload
v-model="ossIdString"
:limit="5"
:fileSize="20"
:fileType="['doc', 'docx', 'pdf', 'xls', 'xlsx']"
:disabled="isView"
:isShowTip="true"
/>
</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="请输入备注" :disabled="isView" />
</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" />
</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 { getCrmCustomerInfoList } from '@/api/oa/crm/customerInfo';
import { getCrmSupplierInfoList } from '@/api/oa/crm/crmSupplierInfo';
import { getBaseUnitInfoList } from '@/api/oa/base/unitInfo';
import { getBasePrintTemplateList } from '@/api/oa/base/printTemplate';
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 } = toRefs<any>(
proxy?.useDict('business_direction', 'currency_type', 'quote_category', 'contract_type', 'material_flag', 'wf_business_status')
);
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,
warrantyPeriod: undefined,
deliveryMethod: undefined,
paymentMethod: undefined,
paymentDetail: undefined,
currencyType: undefined,
taxIncludedInfo: undefined,
taxRate: undefined,
customerId: undefined,
customerName: undefined,
customerContactId: undefined,
customerContactName: undefined,
customerContactPhone: undefined,
customerContactEmail: undefined,
supplierContactId: undefined,
supplierContactName: undefined,
supplierContactPhone: undefined,
supplierContactEmail: undefined,
templateId: undefined,
remark: undefined,
ossId: undefined
});
const rules = {
quoteName: [{ required: true, message: '报价单名称不能为空', trigger: 'blur' }],
customerId: [{ required: true, message: '请选择客户名称', trigger: 'change' }]
};
// 下拉数据
const customerList = ref<any[]>([]);
const customerContactList = ref<any[]>([]);
const supplierList = ref<any[]>([]);
const printTemplateList = ref<any[]>([]);
const DEFAULT_SUPPLIER_ID = 1;
const filteredCustomerContactList = computed(() => {
if (!form.customerId) {
return customerContactList.value;
}
return customerContactList.value.filter((item: any) => String(item.customerId) === String(form.customerId));
});
const paymentMethodOptions = ['电汇', '银行承兑6个月内', '电汇/银行承兑6个月内', '商业承兑'];
const setQuoteDateToday = () => {
const d = new Date();
form.quoteDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} 00:00:00`;
};
// 单位下拉数据
const unitInfoList = ref<any[]>([]);
const getUnitInfoListSelect = async () => {
const res = await getBaseUnitInfoList(null);
unitInfoList.value = res.data || [];
};
const getPrintTemplateListSelect = async () => {
const res = await getBasePrintTemplateList({ templateType: '4' });
printTemplateList.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 ensureStandardMaterialProductName = (
row: CrmQuoteMaterialForm & { materialFlag?: string; materialName?: string; saleMaterialName?: string }
) => {
if (row.materialFlag !== '1' || row.productName) {
return;
}
// 标准物料新增时前端先兜底一次,避免用户未手填产品名称时表格显示为空;
// 真正的客户销售物料关联仍以后端保存逻辑为准,前后端共同保证不漏建。
row.productName = row.saleMaterialName || row.materialName || undefined;
};
const saleMaterialSelectCallBack = (data: any) => {
const list = data || [];
if (list.length) {
const m = list[0];
materialForm.value.materialFlag = '1';
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.unitId) {
materialForm.value.unitId = m.unitId;
}
if (m.unitName) {
materialForm.value.unitName = m.unitName;
}
ensureStandardMaterialProductName(materialForm.value as any);
}
};
const handleMaterialFlagChange = (flag: string) => {
if (flag === '1') {
return;
}
// 切换为非标物料时清空标准物料链路字段,避免把历史 SAP/销售物料关联误当成当前业务数据提交。
materialForm.value.materialId = undefined;
materialForm.value.relationMaterialId = undefined;
materialForm.value.materialCode = undefined;
materialForm.value.materialName = undefined;
materialForm.value.saleMaterialName = undefined;
};
const handleMaterialProductNameInput = (value: string) => {
materialForm.value.productName = value;
if (materialForm.value.materialFlag !== '1' || !materialForm.value.materialId) {
return;
}
// 标准物料允许人工改“产品名称”,一旦与当前销售物料名称不同,就先让旧关联失效,
// 保存时由后端按“物料 + 客户 + 新产品名称”重新查找或自动补建销售物料关联。
if (!value || value !== materialForm.value.saleMaterialName) {
materialForm.value.relationMaterialId = undefined;
materialForm.value.saleMaterialName = undefined;
}
};
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;
const unitInfo = unitInfoList.value.find((item: any) => item.unitId === materialForm.value.unitId);
const materialData = {
...materialForm.value,
unitName: unitInfo?.unitName || materialForm.value.unitName
} as any;
if (materialData.materialFlag !== '1') {
// 非标物料不保留标准物料技术字段,避免后端把非标数据误识别为标准物料联动场景。
materialData.materialId = undefined;
materialData.relationMaterialId = undefined;
materialData.materialCode = undefined;
materialData.materialName = undefined;
materialData.saleMaterialName = undefined;
} else {
ensureStandardMaterialProductName(materialData);
if (!materialData.relationMaterialId && materialData.productName) {
// 保存前的本地表格先展示用户最新输入的产品名称,真正的销售物料关联由后端保存时补齐。
materialData.saleMaterialName = materialData.productName;
}
}
if (currentMaterialIndex.value !== null && currentMaterialIndex.value >= 0) {
materialRows.value[currentMaterialIndex.value] = materialData;
} else {
materialRows.value.push(materialData);
}
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);
if (c?.customerId) {
form.customerId = c.customerId;
form.customerName = c.customerName || form.customerName;
}
form.customerContactName = c?.contactName || '';
form.customerContactPhone = c?.phoneNumber || '';
form.customerContactEmail = c?.email || '';
};
const onCustomerChanged = (customerId: any) => {
const customer = customerList.value.find((item: any) => String(item.customerId) === String(customerId));
form.customerName = customer?.customerName || '';
const currentContact = customerContactList.value.find((item: any) => item.contactId === form.customerContactId);
if (currentContact && String(currentContact.customerId) === String(customerId)) {
return;
}
form.customerContactId = undefined;
form.customerContactName = '';
form.customerContactPhone = '';
form.customerContactEmail = '';
};
// 供应商选择变化处理
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 || '';
}
};
const applyDefaultSupplier = () => {
if (form.supplierContactId) {
return;
}
const supplier = supplierList.value.find((item: any) => item.supplierId === DEFAULT_SUPPLIER_ID);
if (!supplier) {
proxy?.$modal.msgWarning('默认供货方海威不存在,请手工选择供应商');
return;
}
// 新增页默认绑定海威主数据,后续若业务手工切换供应商,联系人信息仍允许按表单继续调整。
form.supplierContactId = supplier.supplierId;
onSupplierChanged(supplier.supplierId);
};
// 生成报价单编号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('报价单编号生成失败');
// }
// };
// 附件ID字符串转换用于FileUpload组件
const ossIdString = computed({
get() {
const v = (form as any).ossId;
return v === undefined || v === null ? '' : String(v);
},
set(val: string) {
(form as any).ossId = val || undefined;
}
});
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: form.quoteName + '审批',
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);
}
};
/** 报价单查看/PDF预览 */
const handleQuoteTemplateDownload = () => {
if (!form.quoteId) {
proxy?.$modal.msgWarning('请先保存报价单后再查看PDF');
return;
}
if (!form.templateId) {
proxy?.$modal.msgWarning('请先在报价单上选择打印模板');
return;
}
// 这里直接透传业务ID和模板ID到预览页保持与列表页导出链路一致避免两套导出逻辑长期分叉
router.push({
path: '/quote/quoteView',
query: { templateId: form.templateId, quoteId: form.quoteId }
});
};
/** 提交审批回调(与 projectInfo 保持一致) */
const submitCallback = async () => {
await proxy?.$tab.closePage(route as any);
router.go(-1);
};
onMounted(async () => {
// 初始化路由参数(与 projectInfo/projectPurchase 保持一致)
routeParams.value = { ...route.query };
// 下拉数据初始化
const customerRes = await getCrmCustomerInfoList({});
customerList.value = customerRes.data || [];
const contactRes = await getCrmCustomerContactList({});
customerContactList.value = contactRes.data || [];
// 加载供应商列表(计划内供应商)
const supplierRes = await getCrmSupplierInfoList({});
supplierList.value = supplierRes.data || [];
await getUnitInfoListSelect();
await getPrintTemplateListSelect();
// 编辑场景
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;
if (!form.customerId && form.customerContactId) {
const currentContact = customerContactList.value.find((item: any) => item.contactId === form.customerContactId);
if (currentContact?.customerId) {
form.customerId = currentContact.customerId;
form.customerName = currentContact.customerName || form.customerName;
}
}
// 根据已有联系人ID回填姓名、电话、邮箱若后端已返回则保持
if (!form.customerContactPhone || !form.customerContactEmail || !form.customerContactName) {
onCustomerContactChanged(form.customerContactId);
}
if (form.supplierContactId && (!form.supplierContactPhone || !form.supplierContactEmail || !form.supplierContactName)) {
onSupplierChanged(form.supplierContactId);
}
// 仅使用后端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,
materialFlag: r.materialFlag,
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) {
setQuoteDateToday();
applyDefaultSupplier();
isCodeGenerated.value = !!form.quoteCode;
}
});
</script>
<style scoped>
/* 基础信息分区整体居中显示 */
.basic-center {
max-width: 860px;
margin: 0 auto;
}
</style>