|
|
<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>
|