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.

611 lines
26 KiB
Vue

<template>
<div class="p-2">
<!-- 统一表单包裹所有分区便于整体验证 -->
<el-form ref="quoteFormRef" :model="form" :rules="rules" label-width="120px" :disabled="isView">
<el-card shadow="never">
<div style="margin-bottom: 12px; text-align: center; font-weight: bold; font-size: 18px">添加报价单基本信息</div>
<div class="basic-center">
<el-row :gutter="20">
<el-col :span="24">
<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="24">
<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="24">
<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="24">
<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="24">
<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="24">
<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="24">
<el-form-item label="交货方式" prop="deliveryMethod">
<el-input v-model="form.deliveryMethod" placeholder="请输入交货方式" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="付款方式" prop="paymentMethod">
<el-input v-model="form.paymentMethod" placeholder="请输入付款方式" />
</el-form-item>
</el-col>
<el-col :span="24">
<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="24">
<el-form-item label="含税信息" prop="taxIncludedInfo">
<el-input v-model="form.taxIncludedInfo" placeholder="如含13%增值税" />
</el-form-item>
</el-col>
<el-col :span="24">
<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="onSupplierContactChanged">
<el-option v-for="c in supplierContactList" :key="c.contactId" :label="c.contactName + ' - ' + (c.phoneNumber||'')" :value="c.contactId" />
</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>
</div>
</el-card>
<!-- 客户方/供货方信息左右并列对称布局 -->
<el-row :gutter="20" style="margin-top: 20px">
<el-col :span="12">
<el-card shadow="never">
<template #header>
<div style="text-align: left; font-weight: bold; font-size: 18px">客户方信息</div>
</template>
<el-row :gutter="20">
<el-col :span="24">
<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="24">
<el-form-item label="客户联系电话" prop="customerContactPhone">
<el-input v-model="form.customerContactPhone" placeholder="客户联系电话" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="客户邮箱" prop="customerContactEmail">
<el-input v-model="form.customerContactEmail" placeholder="客户邮箱" />
</el-form-item>
</el-col>
</el-row>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never">
<template #header>
<div style="text-align: left; font-weight: bold; font-size: 18px">供货方信息</div>
</template>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="供货方联系人" prop="supplierContactId">
<el-select v-model="form.supplierContactId" filterable placeholder="请选择供货方联系人" @change="onSupplierContactChanged">
<el-option v-for="c in supplierContactList" :key="c.contactId" :label="c.contactName + ' - ' + (c.phoneNumber||'')" :value="c.contactId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="供货方联系电话" prop="supplierContactPhone">
<el-input v-model="form.supplierContactPhone" placeholder="供货方联系电话" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="供货方邮箱" prop="supplierContactEmail">
<el-input v-model="form.supplierContactEmail" placeholder="供货方邮箱" />
</el-form-item>
</el-col>
</el-row>
</el-card>
</el-col>
</el-row>
<!-- 附件独立分区样式参考合同编辑页 -->
<el-card shadow="never" style="margin-top: 20px">
<template #header>
<div style="text-align: left; font-weight: bold; font-size: 18px">附件</div>
</template>
<el-row :gutter="20">
<el-col :span="24">
<!-- 与合同页对齐采用附件表单项与上传按钮 -->
<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-card>
<!-- 备注独立分区 -->
<el-card shadow="never" style="margin-top: 20px">
<template #header>
<div style="text-align: left; font-weight: bold; font-size: 18px">备注</div>
</template>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</el-card>
<!-- 统一表单结束 -->
</el-form>
<!-- 报价物料管理 -->
<el-card shadow="never" style="margin-top: 20px">
<template #header>
<div style="text-align: left; font-weight: bold; font-size: 18px">报价明细表格</div>
</template>
<div style="margin-bottom: 12px">
<el-button type="primary" icon="Plus" @click="addMaterialRow" :disabled="isView">新增物料</el-button>
</div>
<el-table :data="materialRows" border show-summary :summary-method="getSummary">
<el-table-column label="序号" width="80" align="center">
<template #default="scope">{{ scope.$index + 1 }}</template>
</el-table-column>
<el-table-column label="产品名称" min-width="160" align="center">
<template #default="scope">
<el-select v-model="scope.row.materialId" filterable placeholder="选择SAP物料" style="width: 100%" @change="onMaterialChange(scope.row)" :disabled="isView">
<el-option v-for="m in materialList" :key="m.materialId" :label="m.materialName" :value="m.materialId" />
</el-select>
</template>
</el-table-column>
<el-table-column label="单位" width="120" align="center">
<template #default="scope">
<el-input v-model="scope.row.unitName" placeholder="单位" :disabled="isView" />
</template>
</el-table-column>
<el-table-column label="数量" width="160" align="center">
<template #default="scope">
<el-input-number v-model="scope.row.amount" :min="0" :precision="2" style="width: 100%" :disabled="isView"
@update:modelValue="(val:any)=>onAmountChange(scope.row,val)" />
</template>
</el-table-column>
<el-table-column label="未税单价" width="180" align="center">
<template #default="scope">
<el-input-number v-model="scope.row.beforePrice" :min="0" :precision="2" style="width: 100%" :disabled="isView"
@update:modelValue="(val:any)=>onBeforePriceChange(scope.row,val)" />
</template>
</el-table-column>
<el-table-column label="税率(%)" width="160" align="center">
<template #default="scope">
<el-input-number v-model="scope.row.taxRate" :min="0" :precision="2" style="width: 100%" :disabled="isView"
@update:modelValue="(val:any)=>onTaxRateChange(scope.row,val)" />
</template>
</el-table-column>
<el-table-column label="含税单价" width="180" align="center">
<template #default="scope">
<el-input-number v-model="scope.row.includingPrice" :min="0" :precision="2" style="width: 100%" :disabled="isView"
@update:modelValue="(val:any)=>onIncludingPriceChange(scope.row,val)" />
</template>
</el-table-column>
<el-table-column label="小计" width="140" align="center">
<template #default="scope">
{{ (scope.row.subtotal ?? 0).toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="备注" min-width="160" align="center">
<template #default="scope">
<el-input v-model="scope.row.remark" :disabled="isView" />
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center">
<template #default="scope">
<el-button v-if="!isView" link type="danger" icon="Delete" @click="removeRow(scope.$index)"></el-button>
</template>
</el-table-column>
</el-table>
<div style="margin-top: 12px; text-align: right">
<span style="margin-right: 16px">含税总价{{ totalIncludingTax.toFixed(2) }}</span>
<el-button v-if="!isView" type="primary" :loading="buttonLoading" @click="submitForm"> </el-button>
<el-button @click="goBack"> </el-button>
</div>
</el-card>
<!-- 附件上传对话框 -->
<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 { getCrmQuoteInfo, addCrmQuoteInfo, updateCrmQuoteInfo, recalcQuoteTotals } from '@/api/oa/crm/crmQuoteInfo';
import { CrmQuoteInfoForm } from '@/api/oa/crm/crmQuoteInfo/types';
import type { CrmQuoteMaterialForm } from '@/api/oa/crm/crmQuoteMaterial/types';
import { getBaseMaterialInfoList } from '@/api/oa/base/materialInfo';
import { getCrmCustomerContactList } from '@/api/oa/crm/customerContact';
import { getRuleGenerateCode } from '@/api/system/codeRule';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { business_direction, currency_type, quote_category, contract_type } = toRefs<any>(proxy?.useDict('business_direction', 'currency_type', 'quote_category', 'contract_type'));
const router = useRouter();
const route = useRoute();
// 查看模式根据路由type=view禁用输入与保存
const isView = computed(() => route.query.type === 'view');
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 materialList = ref<any[]>([]);
const customerContactList = ref<any[]>([]);
const supplierContactList = ref<any[]>([]);
// 物料行
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({ amount: 0, beforePrice: 0, taxRate: Number(form.taxRate || 0), includingPrice: 0, subtotal: 0 } 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 onSupplierContactChanged = (id: any) => {
const c = supplierContactList.value.find((x: any) => x.contactId === id);
form.supplierContactName = c?.contactName || '';
form.supplierContactPhone = c?.phoneNumber || '';
form.supplierContactEmail = c?.email || '';
};
const onMaterialChange = (row: any) => {
// 可扩展:根据选中物料带出单位等
};
// 生成报价单编号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 = () => {
quoteFormRef.value?.validate(async (valid: boolean) => {
if (!valid) return;
buttonLoading.value = true;
try {
// 携带itemsBo一次性保存由后端主子表事务处理
const payload: any = {
...form,
itemsBo: materialRows.value.map((r, idx) => ({
...r,
itemNo: idx + 1,
subtotal: Number(r.subtotal || 0)
}))
};
if (!form.quoteId) {
await addCrmQuoteInfo(payload);
} else {
await updateCrmQuoteInfo(payload);
}
proxy?.$modal.msgSuccess('保存成功');
router.back();
} finally {
buttonLoading.value = false;
}
});
};
const goBack = () => router.back();
onMounted(async () => {
// 下拉数据初始化
const materialRes = await getBaseMaterialInfoList({});
materialList.value = materialRes.data || [];
const contactRes = await getCrmCustomerContactList({});
customerContactList.value = contactRes.data || [];
supplierContactList.value = contactRes.data || [];
// 编辑场景
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.supplierContactPhone || !form.supplierContactEmail || !form.supplierContactName) {
onSupplierContactChanged(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,
materialId: r.materialId,
unitId: r.unitId,
unitName: r.unitName,
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>