| src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/collaborativeApproval/approvalProcess/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/salesManagement/indicatorStats/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/salesManagement/receiptPaymentLedger/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/salesManagement/salesLedger/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/salesManagement/salesQuotation/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue
@@ -32,7 +32,7 @@ </el-form-item> </el-col> </el-row> <el-row> <el-row v-if="!isQuotationApproval"> <el-col :span="24"> <el-form-item label="审批事由:" prop="approveReason"> <el-input v-model="form.approveReason" placeholder="请输入" clearable type="textarea" disabled/> @@ -73,6 +73,54 @@ </el-col> </el-row> </el-form> <!-- 报价审批:展示报价详情(复用销售报价“查看详情对话框”内容结构) --> <div v-if="isQuotationApproval" style="margin: 10px 0 18px;"> <el-divider content-position="left">报价详情</el-divider> <el-skeleton :loading="quotationLoading" animated> <template #template> <el-skeleton-item variant="h3" style="width: 30%" /> <el-skeleton-item variant="text" style="width: 100%" /> <el-skeleton-item variant="text" style="width: 100%" /> </template> <template #default> <el-empty v-if="!currentQuotation || !currentQuotation.quotationNo" description="未查询到对应报价详情" /> <template v-else> <el-descriptions :column="2" border> <el-descriptions-item label="报价单号">{{ currentQuotation.quotationNo }}</el-descriptions-item> <el-descriptions-item label="客户名称">{{ currentQuotation.customer }}</el-descriptions-item> <el-descriptions-item label="业务员">{{ currentQuotation.salesperson }}</el-descriptions-item> <el-descriptions-item label="报价日期">{{ currentQuotation.quotationDate }}</el-descriptions-item> <el-descriptions-item label="有效期至">{{ currentQuotation.validDate }}</el-descriptions-item> <el-descriptions-item label="付款方式">{{ currentQuotation.paymentMethod }}</el-descriptions-item> <el-descriptions-item label="报价总额" :span="2"> <span style="font-size: 18px; color: #e6a23c; font-weight: bold;"> ¥{{ Number(currentQuotation.totalAmount ?? 0).toFixed(2) }} </span> </el-descriptions-item> </el-descriptions> <div style="margin-top: 20px;"> <h4>产品明细</h4> <el-table :data="currentQuotation.products || []" border style="width: 100%"> <el-table-column prop="product" label="产品名称" /> <el-table-column prop="specification" label="规格型号" /> <el-table-column prop="unit" label="单位" /> <el-table-column prop="unitPrice" label="单价"> <template #default="scope">¥{{ Number(scope.row.unitPrice ?? 0).toFixed(2) }}</template> </el-table-column> </el-table> </div> <div v-if="currentQuotation.remark" style="margin-top: 20px;"> <h4>备注</h4> <p>{{ currentQuotation.remark }}</p> </div> </template> </template> </el-skeleton> </div> <el-form :model="{ activities }" ref="formRef" label-position="top"> <el-steps :active="getActiveStep()" finish-status="success" process-status="process" align-center direction="vertical"> <el-step @@ -130,7 +178,7 @@ </template> <script setup> import { getCurrentInstance, reactive, ref, toRefs } from "vue"; import { computed, getCurrentInstance, reactive, ref, toRefs } from "vue"; import { approveProcessDetails, getDept, @@ -139,8 +187,16 @@ import useUserStore from "@/store/modules/user.js"; import {userListNoPageByTenantId} from "@/api/system/user.js"; import { WarningFilled, Edit, Check, MoreFilled } from '@element-plus/icons-vue' import { getQuotationList } from "@/api/salesManagement/salesQuotation.js"; const emit = defineEmits(['close']) const { proxy } = getCurrentInstance() const props = defineProps({ approveType: { type: [Number, String], default: 0 } }) const dialogFormVisible = ref(false); const operationType = ref('') @@ -149,6 +205,10 @@ const userStore = useUserStore() const productOptions = ref([]); const userList = ref([]) const quotationLoading = ref(false) const currentQuotation = ref({}) const isQuotationApproval = computed(() => Number(props.approveType) === 6) const data = reactive({ form: { approveTime: "", @@ -186,11 +246,27 @@ const openDialog = (type, row) => { operationType.value = type; dialogFormVisible.value = true; currentQuotation.value = {} userListNoPageByTenantId().then((res) => { userList.value = res.data; }); form.value = {...row} getProductOptions() // 报价审批:用审批事由字段承载的“报价单号”去查报价列表 if (isQuotationApproval.value) { const quotationNo = row?.approveReason; if (quotationNo) { quotationLoading.value = true getQuotationList({ quotationNo }).then((res) => { const records = res?.data?.records || [] currentQuotation.value = records[0] || {} }).finally(() => { quotationLoading.value = false }) } } approveProcessDetails(row.approveId).then((res) => { activities.value = res.data // 增加isApproval字段 @@ -230,6 +306,8 @@ const closeDia = () => { proxy.resetForm("formRef"); dialogFormVisible.value = false; quotationLoading.value = false currentQuotation.value = {} emit('close') }; defineExpose({ src/views/collaborativeApproval/approvalProcess/index.vue
@@ -35,7 +35,7 @@ > </div> <div> <el-button type="primary" @click="openForm('add')">新增</el-button> <el-button type="primary" @click="openForm('add')" v-if="currentApproveType !== 6">新增</el-button> <el-button @click="handleOut">导出</el-button> <el-button type="danger" plain @click="handleDelete">删除</el-button> </div> @@ -54,7 +54,7 @@ ></PIMTable> </div> <info-form-dia ref="infoFormDia" @close="handleQuery" :approveType="currentApproveType"></info-form-dia> <approval-dia ref="approvalDia" @close="handleQuery"></approval-dia> <approval-dia ref="approvalDia" @close="handleQuery" :approveType="currentApproveType"></approval-dia> <FileList ref="fileListRef" /> </div> </template> @@ -103,6 +103,7 @@ const tableColumnCopy = computed(() => { const isLeaveType = currentApproveType.value === 2; // 请假管理 const isReimburseType = currentApproveType.value === 4; // 报销管理 const isQuotationType = currentApproveType.value === 6; // 报价审批 // 基础列配置 const baseColumns = [ @@ -149,7 +150,7 @@ width: 220 }, { label: "审批事由", label: isQuotationType ? "报价单号" : "审批事由", prop: "approveReason", width: 200 }, @@ -204,7 +205,7 @@ clickFun: (row) => { openForm("edit", row); }, disabled: (row) => row.approveStatus == 2 || row.approveStatus == 1 || row.approveStatus == 4 disabled: (row) => currentApproveType.value === 6 || row.approveStatus == 2 || row.approveStatus == 1 || row.approveStatus == 4 }, { name: "审核", src/views/salesManagement/indicatorStats/index.vue
@@ -3,7 +3,7 @@ <el-card class="box-card"> <!-- KPI 汇总 --> <el-row :gutter="20" class="stats-row"> <el-col :span="6"> <el-col :span="8"> <div class="stat-card"> <div class="stat-icon" style="background: #ecf5ff;"> <el-icon :size="30" color="#409eff"><Document /></el-icon> @@ -14,7 +14,7 @@ </div> </div> </el-col> <el-col :span="6"> <el-col :span="8"> <div class="stat-card"> <div class="stat-icon" style="background: #f0f9ff;"> <el-icon :size="30" color="#67c23a"><Tickets /></el-icon> @@ -25,7 +25,7 @@ </div> </div> </el-col> <el-col :span="6"> <el-col :span="8"> <div class="stat-card"> <div class="stat-icon" style="background: #fef0f0;"> <el-icon :size="30" color="#e6a23c"><Van /></el-icon> @@ -33,17 +33,6 @@ <div class="stat-content"> <div class="stat-value">{{ indicatorKpis.shipmentRate }}%</div> <div class="stat-label">发货率</div> </div> </div> </el-col> <el-col :span="6"> <div class="stat-card"> <div class="stat-icon" style="background: #f4f4f5;"> <el-icon :size="30" color="#f56c6c"><Wallet /></el-icon> </div> <div class="stat-content"> <div class="stat-value">{{ indicatorKpis.collectionRate }}%</div> <div class="stat-label">回款率</div> </div> </div> </el-col> @@ -93,7 +82,7 @@ </div> <!-- 业绩统计(团队维度,无个人姓名) --> <el-table :data="teamPerformanceList" border stripe style="margin-top: 20px;"> <el-table v-if="showTeamPerformance" :data="teamPerformanceList" border stripe style="margin-top: 20px;"> <el-table-column prop="team" label="销售团队"/> <el-table-column prop="orderCount" label="订单数"/> <el-table-column prop="salesAmount" label="销售额"> @@ -101,9 +90,6 @@ </el-table-column> <el-table-column prop="shipmentRate" label="发货率"> <template #default="scope">{{ scope.row.shipmentRate }}%</template> </el-table-column> <el-table-column prop="collectionRate" label="回款率"> <template #default="scope">{{ scope.row.collectionRate }}%</template> </el-table-column> <el-table-column prop="attainment" label="目标达成率"> <template #default="scope"> @@ -119,15 +105,17 @@ <script setup> import { ref, reactive, onMounted, nextTick } from 'vue' import { Document, Van, Tickets, Wallet } from '@element-plus/icons-vue' import { Document, Van, Tickets } from '@element-plus/icons-vue' import * as echarts from 'echarts' const indicatorKpis = reactive({ orderCount: 1280, salesAmount: 9650000, shipmentRate: 89.2, collectionRate: 76.4 shipmentRate: 89.2 }) // 是否展示销售团队明细表,按需开启 const showTeamPerformance = ref(false) const indicatorFilter = reactive({ product: '', @@ -140,10 +128,10 @@ let indicatorChart = null const teamPerformanceList = ref([ { team: '华东大区', orderCount: 320, salesAmount: 2850000, shipmentRate: 90, collectionRate: 80, attainment: 105 }, { team: '华北大区', orderCount: 280, salesAmount: 2150000, shipmentRate: 86, collectionRate: 73, attainment: 92 }, { team: '华南大区', orderCount: 210, salesAmount: 1850000, shipmentRate: 88, collectionRate: 70, attainment: 78 }, { team: '西南大区', orderCount: 180, salesAmount: 1500000, shipmentRate: 83, collectionRate: 68, attainment: 74 } { team: '华东大区', orderCount: 320, salesAmount: 2850000, shipmentRate: 90, attainment: 105 }, { team: '华北大区', orderCount: 280, salesAmount: 2150000, shipmentRate: 86, attainment: 92 }, { team: '华南大区', orderCount: 210, salesAmount: 1850000, shipmentRate: 88, attainment: 78 }, { team: '西南大区', orderCount: 180, salesAmount: 1500000, shipmentRate: 83, attainment: 74 } ]) const initIndicatorChart = () => { @@ -153,7 +141,7 @@ const option = { title: { text: '多维度销售指标趋势', left: 'center' }, tooltip: { trigger: 'axis' }, legend: { data: ['订单数', '销售额', '发货率', '回款率'], top: 30 }, legend: { data: ['订单数', '销售额', '发货率'], top: 30 }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'category', data: ['2024-12', '2025-01', '2025-02', '2025-03', '2025-04', '2025-05'] }, yAxis: [ @@ -163,8 +151,7 @@ series: [ { name: '订单数', type: 'bar', data: [180, 220, 210, 260, 205, 225], itemStyle: { color: '#409eff' } }, { name: '销售额', type: 'bar', data: [820, 950, 910, 1080, 980, 1020], itemStyle: { color: '#67c23a' } }, { name: '发货率', type: 'line', yAxisIndex: 1, data: [86, 89, 88, 91, 87, 90], itemStyle: { color: '#e6a23c' } }, { name: '回款率', type: 'line', yAxisIndex: 1, data: [72, 76, 74, 79, 75, 78], itemStyle: { color: '#f56c6c' } } { name: '发货率', type: 'line', yAxisIndex: 1, data: [86, 89, 88, 91, 87, 90], itemStyle: { color: '#e6a23c' } } ] } indicatorChart.setOption(option) @@ -178,7 +165,6 @@ indicatorKpis.orderCount = random(1280, 120) indicatorKpis.salesAmount = random(9650000, 350000) indicatorKpis.shipmentRate = (85 + Math.random() * 10).toFixed(1) * 1 indicatorKpis.collectionRate = (70 + Math.random() * 12).toFixed(1) * 1 setTimeout(() => initIndicatorChart(), 200) } @@ -191,13 +177,12 @@ } const exportIndicatorTable = () => { const header = ['销售团队', '订单数', '销售额', '发货率(%)', '回款率(%)', '目标达成率(%)'] const header = ['销售团队', '订单数', '销售额', '发货率(%)', '目标达成率(%)'] const rows = teamPerformanceList.value.map(r => [ r.team, r.orderCount, r.salesAmount, r.shipmentRate, r.collectionRate, r.attainment ]) const csv = [header, ...rows].map(r => r.join(',')).join('\n') src/views/salesManagement/receiptPaymentLedger/index.vue
@@ -41,7 +41,7 @@ width="200" /> <el-table-column label="开票金额(元)" label="合同金额(元)" prop="invoiceTotal" show-overflow-tooltip :formatter="formattedNumber" @@ -93,33 +93,39 @@ /> <el-table-column label="发生日期" prop="happenTime" prop="receiptPaymentDate" show-overflow-tooltip width="110" /> <el-table-column label="开票金额(元)" prop="invoiceAmount" label="销售合同号" prop="salesContractNo" show-overflow-tooltip width="200" /> <el-table-column label="合同金额(元)" prop="invoiceTotal" show-overflow-tooltip :formatter="formattedNumber" width="200" /> <el-table-column label="回款金额(元)" prop="receiptAmount" prop="receiptPaymentAmount" show-overflow-tooltip :formatter="formattedNumber" width="200" /> <el-table-column label="应收金额(元)" prop="unReceiptAmount" prop="unReceiptPaymentAmount" show-overflow-tooltip width="200" > <template #default="{ row, column }"> <el-text type="danger"> {{ formattedNumber(row, column, row.unReceiptAmount) }} {{ formattedNumber(row, column, row.unReceiptPaymentAmount) }} </el-text> </template> </el-table-column> src/views/salesManagement/salesLedger/index.vue
@@ -98,6 +98,11 @@ @confirm="submitForm" @cancel="closeDia"> <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef"> <el-row v-if="operationType !== 'view'"> <el-col :span="24" style="display:flex; justify-content:flex-end; gap:10px; margin-bottom: 6px;"> <el-button type="primary" plain @click="openQuotationDialog">从审批通过的报价单导入</el-button> </el-col> </el-row> <el-row :gutter="30"> <el-col :span="12"> <el-form-item label="销售合同号:" prop="salesContractNo"> @@ -203,6 +208,62 @@ </el-row> </el-form> </FormDialog> <!-- 从报价单导入(仅审批通过) --> <el-dialog v-model="quotationDialogVisible" title="选择审批通过的销售报价单" width="80%" :close-on-click-modal="false" > <div style="margin-bottom: 12px; display:flex; gap: 12px; align-items:center;"> <el-input v-model="quotationSearchForm.quotationNo" placeholder="请输入报价单号" clearable style="max-width: 260px;" @change="fetchQuotationList" /> <el-input v-model="quotationSearchForm.customer" placeholder="请输入客户名称" clearable style="max-width: 260px;" @change="fetchQuotationList" /> <el-button type="primary" @click="fetchQuotationList">搜索</el-button> <el-button @click="resetQuotationSearch">重置</el-button> </div> <el-table :data="quotationList" border stripe v-loading="quotationLoading" height="420px" > <el-table-column align="center" label="序号" type="index" width="60" /> <el-table-column prop="quotationNo" label="报价单号" width="180" show-overflow-tooltip /> <el-table-column prop="customer" label="客户名称" min-width="220" show-overflow-tooltip /> <el-table-column prop="salesperson" label="业务员" width="120" show-overflow-tooltip /> <el-table-column prop="quotationDate" label="报价日期" width="140" /> <el-table-column prop="status" label="审批状态" width="120" align="center" /> <el-table-column prop="totalAmount" label="报价金额(元)" width="160" align="right"> <template #default="scope"> {{ Number(scope.row.totalAmount ?? 0).toFixed(2) }} </template> </el-table-column> <el-table-column fixed="right" label="操作" width="120" align="center"> <template #default="scope"> <el-button type="primary" link @click="applyQuotation(scope.row)">选择</el-button> </template> </el-table-column> </el-table> <template #footer> <el-button @click="quotationDialogVisible = false">关闭</el-button> </template> </el-dialog> <FormDialog v-model="productFormVisible" :title="productOperationType === 'add' ? '新增产品' : '编辑产品'" @@ -461,6 +522,7 @@ import { userListNoPage } from "@/api/system/user.js"; import FileListDialog from '@/components/Dialog/FileListDialog.vue'; import FormDialog from '@/components/Dialog/FormDialog.vue'; import { getQuotationList } from "@/api/salesManagement/salesQuotation.js"; import { ledgerListPage, productList, @@ -576,6 +638,16 @@ const printPreviewVisible = ref(false); const printData = ref([]); // 报价单导入相关 const quotationDialogVisible = ref(false); const quotationLoading = ref(false); const quotationList = ref([]); const quotationSearchForm = reactive({ quotationNo: "", customer: "", }); const selectedQuotation = ref(null); // 发货相关 const deliveryFormVisible = ref(false); const currentDeliveryRow = ref(null); @@ -644,8 +716,10 @@ }; // 获取产品大类tree数据 const getProductOptions = () => { productTreeList().then((res) => { // 返回 Promise,便于在编辑产品时等待加载完成 return productTreeList().then((res) => { productOptions.value = convertIdToValue(res); return productOptions.value; }); }; const formattedNumber = (row, column, cellValue) => { @@ -695,6 +769,19 @@ return newItem; }); } // 根据名称反查产品大类 id,便于仅存名称时的反显 function findNodeIdByLabel(nodes, label) { if (!label) return null; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (node.label === label) return node.value; if (node.children && node.children.length > 0) { const found = findNodeIdByLabel(node.children, label); if (found !== null && found !== undefined) return found; } } return null; } // 表格选择数据 const handleSelectionChange = (selection) => { @@ -746,6 +833,7 @@ operationType.value = type; form.value = {}; productData.value = []; selectedQuotation.value = null; let userLists = await userListNoPage(); userList.value = userLists.data; customerList().then((res) => { @@ -774,6 +862,87 @@ // }); form.value.entryDate = getCurrentDate(); // 设置默认录入日期为当前日期 dialogFormVisible.value = true; }; // 打开报价单选择弹窗(仅审批通过) const openQuotationDialog = async () => { if (operationType.value === "view") return; quotationDialogVisible.value = true; // 先确保客户列表已加载,便于后续回填 customerId if (!customerOption.value || customerOption.value.length === 0) { try { const res = await customerList(); customerOption.value = res; } catch (e) { // ignore,允许用户后续手动选择客户 } } await fetchQuotationList(); }; const fetchQuotationList = async () => { quotationLoading.value = true; try { const params = { // 兼容后端分页字段:这里沿用报价页面已有可用的字段命名 currentPage: 1, pageSize: 100, ...quotationSearchForm, status: "通过", }; const res = await getQuotationList(params); quotationList.value = res?.data?.records || []; } finally { quotationLoading.value = false; } }; const resetQuotationSearch = async () => { quotationSearchForm.quotationNo = ""; quotationSearchForm.customer = ""; await fetchQuotationList(); }; // 选中报价单后回填到台账表单 const applyQuotation = (row) => { if (!row) return; selectedQuotation.value = row; // 业务员 form.value.salesman = row.salesperson || ""; // 客户名称 -> customerId const customer = (customerOption.value || []).find((c) => c.customerName === row.customer); if (customer?.id) { form.value.customerId = customer.id; } else { // 如果找不到,保留原值(允许用户手动选择/不打断已有输入) form.value.customerId = form.value.customerId || ""; } // 产品信息映射:报价 products -> 台账 productData const products = Array.isArray(row.products) ? row.products : []; productData.value = products.map((p) => { const quantity = Number(p.quantity ?? 0) || 0; const unitPrice = Number(p.unitPrice ?? 0) || 0; const taxRate = "13"; // 默认 13%,便于直接提交(如需可在产品中自行修改) const taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2); const taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(taxInclusiveTotalPrice, taxRate); return { // 台账字段 productCategory: p.product || p.productName || "", specificationModel: p.specification || "", unit: p.unit || "", quantity: quantity, taxRate: taxRate, taxInclusiveUnitPrice: unitPrice.toFixed(2), taxInclusiveTotalPrice: taxInclusiveTotalPrice, taxExclusiveTotalPrice: taxExclusiveTotalPrice, invoiceType: "增普票", }; }); quotationDialogVisible.value = false; }; function changs(val) { console.log(val); @@ -847,16 +1016,36 @@ const productIndex = ref(0); // 打开产品弹框 const openProductForm = (type, row,index) => { const openProductForm = async (type, row, index) => { productOperationType.value = type; productForm.value = {}; proxy.resetForm("productFormRef"); if (type === "edit") { productForm.value = { ...row }; productIndex.value = index; // 编辑时根据产品大类名称反查 tree 节点 id,并加载规格型号列表 try { const options = productOptions.value && productOptions.value.length > 0 ? productOptions.value : await getProductOptions(); const categoryId = findNodeIdByLabel(options, productForm.value.productCategory); if (categoryId) { const models = await modelList({ id: categoryId }); modelOptions.value = models || []; // 根据当前规格型号名称反查并设置 productModelId,便于下拉框显示已选值 const currentModel = (modelOptions.value || []).find( (m) => m.model === productForm.value.specificationModel ); if (currentModel) { productForm.value.productModelId = currentModel.id; } } } catch (e) { // 加载失败时保持可编辑,不中断弹窗 console.error("加载产品规格型号失败", e); } } productFormVisible.value = true; getProductOptions(); }; // 提交产品表单 const submitProduct = () => { src/views/salesManagement/salesQuotation/index.vue
@@ -56,16 +56,23 @@ <el-table-column prop="salesperson" label="业务员" width="100" /> <el-table-column prop="quotationDate" label="报价日期" width="120" /> <el-table-column prop="validDate" label="有效期至" width="120" /> <el-table-column prop="status" label="审批状态" width="120" align="center"> <template #default="{ row }"> <el-tag :type="getStatusType(row.status)" disable-transitions> {{ row.status || '--' }} </el-tag> </template> </el-table-column> <el-table-column prop="totalAmount" label="报价金额" width="120"> <template #default="scope"> ¥{{ scope.row.totalAmount.toFixed(2) }} </template> </el-table-column> <el-table-column label="操作" width="250" fixed="right" align="center"> <el-table-column label="操作" width="200" fixed="right" align="center"> <template #default="scope"> <el-button link type="primary" @click="handleView(scope.row)">查看</el-button> <el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.status === '草稿'">编辑</el-button> <el-button link type="danger" @click="handleDelete(scope.row)" v-if="scope.row.status === '草稿'">删除</el-button> <el-button link type="primary" @click="handleEdit(scope.row)" :disabled="!['待审批','拒绝'].includes(scope.row.status)">编辑</el-button> <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button> </template> </el-table-column> </el-table> @@ -81,82 +88,147 @@ </el-card> <!-- 新增/编辑对话框 --> <FormDialog v-model="dialogVisible" :title="dialogTitle" width="80%" :close-on-click-modal="false" @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false"> <el-form :model="form" :rules="rules" ref="formRef" label-width="100px"> <FormDialog v-model="dialogVisible" :title="dialogTitle" width="85%" :close-on-click-modal="false" @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false"> <div class="quotation-form-container"> <el-form :model="form" :rules="rules" ref="formRef" label-width="120px" class="quotation-form"> <!-- 基本信息 --> <el-card class="form-card" shadow="never"> <el-card class="form-card" shadow="hover"> <template #header> <span class="card-title">基本信息</span> <div class="card-header-wrapper"> <el-icon class="card-icon"><Document /></el-icon> <span class="card-title">基本信息</span> </div> </template> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="客户名称" prop="customer"> <el-select v-model="form.customer" placeholder="请选择客户" style="width: 100%" @change="handleCustomerChange"> <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.customerName"> {{ item.customerName + "——" + item.taxpayerIdentificationNumber }} </el-option> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="业务员" prop="salesperson"> <el-select v-model="form.salesperson" placeholder="请选择业务员" style="width: 100%"> <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName" /> </el-select> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="报价日期" prop="quotationDate"> <el-date-picker v-model="form.quotationDate" type="date" placeholder="选择报价日期" style="width: 100%" format="YYYY-MM-DD" value-format="YYYY-MM-DD" /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="有效期至" prop="validDate"> <el-date-picker v-model="form.validDate" type="date" placeholder="选择有效期" style="width: 100%" format="YYYY-MM-DD" value-format="YYYY-MM-DD" /> </el-form-item> </el-col> </el-row> <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="付款方式" prop="paymentMethod"> <el-select v-model="form.paymentMethod" placeholder="请选择付款方式" style="width: 100%"> <el-option label="全款到付" value="全款到付"></el-option> <el-option label="分期付款" value="分期付款"></el-option> <el-option label="月结" value="月结"></el-option> </el-select> </el-form-item> </el-col> </el-row> <div class="form-content"> <el-row :gutter="24"> <el-col :span="12"> <el-form-item label="客户名称" prop="customer"> <el-select v-model="form.customer" placeholder="请选择客户" style="width: 100%" @change="handleCustomerChange" clearable> <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.customerName"> {{ item.customerName + "——" + item.taxpayerIdentificationNumber }} </el-option> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="业务员" prop="salesperson"> <el-select v-model="form.salesperson" placeholder="请选择业务员" style="width: 100%" clearable> <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName" /> </el-select> </el-form-item> </el-col> </el-row> <el-row :gutter="24"> <el-col :span="12"> <el-form-item label="报价日期" prop="quotationDate"> <el-date-picker v-model="form.quotationDate" type="date" placeholder="选择报价日期" style="width: 100%" format="YYYY-MM-DD" value-format="YYYY-MM-DD" clearable /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="有效期至" prop="validDate"> <el-date-picker v-model="form.validDate" type="date" placeholder="选择有效期" style="width: 100%" format="YYYY-MM-DD" value-format="YYYY-MM-DD" clearable /> </el-form-item> </el-col> </el-row> <el-row :gutter="24"> <el-col :span="12"> <el-form-item label="付款方式" prop="paymentMethod"> <el-input v-model="form.paymentMethod" placeholder="请输入付款方式" clearable /> </el-form-item> </el-col> </el-row> </div> </el-card> <!-- 审批人信息 --> <el-card class="form-card" shadow="hover"> <template #header> <div class="card-header-wrapper"> <el-icon class="card-icon"><UserFilled /></el-icon> <span class="card-title">审批人选择</span> <el-button type="primary" size="small" @click="addApproverNode" class="header-btn"> <el-icon><Plus /></el-icon> 新增节点 </el-button> </div> </template> <div class="form-content"> <el-row> <el-col :span="24"> <el-form-item> <div class="approver-nodes-container"> <div v-for="(node, index) in approverNodes" :key="node.id" class="approver-node-item" > <div class="approver-node-label"> <span class="node-step">{{ index + 1 }}</span> <span class="node-text">审批人</span> <el-icon class="arrow-icon"><ArrowRight /></el-icon> </div> <el-select v-model="node.userId" placeholder="选择人员" class="approver-select" clearable > <el-option v-for="user in userList" :key="user.userId" :label="user.nickName" :value="user.userId" /> </el-select> <el-button type="danger" size="small" :icon="Delete" @click="removeApproverNode(index)" v-if="approverNodes.length > 1" class="remove-btn" >删除</el-button> </div> </div> </el-form-item> </el-col> </el-row> </div> </el-card> <!-- 产品信息 --> <el-card class="form-card" shadow="never"> <el-card class="form-card" shadow="hover"> <template #header> <div class="card-header"> <div class="card-header-wrapper"> <el-icon class="card-icon"><Box /></el-icon> <span class="card-title">产品信息</span> <el-button type="primary" size="small" @click="addProduct">添加产品</el-button> <el-button type="primary" size="small" @click="addProduct" class="header-btn"> <el-icon><Plus /></el-icon> 添加产品 </el-button> </div> </template> <el-table :data="form.products" border style="width: 100%"> <div class="form-content"> <el-table :data="form.products" border style="width: 100%" class="product-table" v-if="form.products.length > 0"> <el-table-column prop="product" label="产品名称" width="200"> <template #default="scope"> <el-tree-select @@ -204,18 +276,33 @@ </template> </el-table-column> </el-table> <el-empty v-else description="暂无产品,请点击添加产品" :image-size="80" /> </div> </el-card> <!-- 备注信息 --> <el-card class="form-card" shadow="never"> <el-card class="form-card" shadow="hover"> <template #header> <span class="card-title">备注信息</span> <div class="card-header-wrapper"> <el-icon class="card-icon"><EditPen /></el-icon> <span class="card-title">备注信息</span> </div> </template> <el-form-item label="备注" prop="remark"> <el-input type="textarea" v-model="form.remark" placeholder="请输入备注信息" rows="3"></el-input> </el-form-item> <div class="form-content"> <el-form-item label="备注" prop="remark"> <el-input type="textarea" v-model="form.remark" placeholder="请输入备注信息(选填)" :rows="4" maxlength="500" show-word-limit ></el-input> </el-form-item> </div> </el-card> </el-form> </div> </FormDialog> <!-- 查看详情对话框 --> @@ -260,7 +347,7 @@ <script setup> import { ref, reactive, computed, onMounted, markRaw, shallowRef } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { Search } from '@element-plus/icons-vue' import { Search, Document, UserFilled, Box, EditPen, Plus, ArrowRight, Delete } from '@element-plus/icons-vue' import Pagination from '@/components/PIMTable/Pagination.vue' import FormDialog from '@/components/Dialog/FormDialog.vue' import {getQuotationList,addQuotation,updateQuotation,deleteQuotation} from '@/api/salesManagement/salesQuotation.js' @@ -311,15 +398,31 @@ salesperson: [{ required: true, message: '请选择业务员', trigger: 'change' }], quotationDate: [{ required: true, message: '请选择报价日期', trigger: 'change' }], validDate: [{ required: true, message: '请选择有效期', trigger: 'change' }], paymentMethod: [{ required: true, message: '请选择付款方式', trigger: 'change' }] paymentMethod: [{ required: true, message: '请输入付款方式', trigger: 'blur' }] } const userList = ref([]); const customerOption = ref([]); // 审批人节点相关 const approverNodes = ref([ { id: 1, userId: null } ]) let nextApproverId = 2 const isEdit = ref(false) const editId = ref(null) const currentQuotation = ref({}) const formRef = ref() // 添加审批人节点 function addApproverNode() { approverNodes.value.push({ id: nextApproverId++, userId: null }) } // 删除审批人节点 function removeApproverNode(index) { approverNodes.value.splice(index, 1) } // 计算属性 const filteredList = computed(() => { @@ -339,10 +442,10 @@ // 方法 const getStatusType = (status) => { const statusMap = { '草稿': 'info', '已发送': 'primary', '客户确认': 'success', '已过期': 'danger' '待审批': 'info', '审核中': 'primary', '通过': 'success', '拒绝': 'danger' } return statusMap[status] || 'info' } @@ -357,6 +460,9 @@ dialogTitle.value = '新增报价' isEdit.value = false resetForm() // 重置审批人节点 approverNodes.value = [{ id: 1, userId: null }] nextApproverId = 2 dialogVisible.value = true let userLists = await userListNoPage(); // 只复制需要的字段,避免将组件引用放入响应式对象 @@ -376,8 +482,10 @@ }); } const getProductOptions = () => { productTreeList().then((res) => { // 返回 Promise,便于编辑时 await 确保能反显 return productTreeList().then((res) => { productOptions.value = convertIdToValue(res); return productOptions.value }); }; function convertIdToValue(data) { @@ -393,6 +501,19 @@ return newItem; }); } // 根据名称反查节点 id,便于仅存名称时的反显 function findNodeIdByLabel(nodes, label) { if (!label) return null; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (node.label === label) return node.value; if (node.children && node.children.length > 0) { const found = findNodeIdByLabel(node.children, label); if (found !== null && found !== undefined) return found; } } return null; } const getModels = (value, row) => { if (!row) return; @@ -478,10 +599,14 @@ viewDialogVisible.value = true } const handleEdit = (row) => { const handleEdit = async (row) => { dialogTitle.value = '编辑报价' isEdit.value = true editId.value = row.id form.id = row.id || form.id || null // 先加载产品树数据,否则 el-tree-select 无法反显产品名称 await getProductOptions() // 只复制需要的字段,避免将组件引用放入响应式对象 form.quotationNo = row.quotationNo || '' form.customer = row.customer || '' @@ -491,22 +616,51 @@ form.paymentMethod = row.paymentMethod || '' form.status = row.status || '草稿' form.remark = row.remark || '' form.products = row.products ? row.products.map(product => ({ productId: product.productId || '', product: product.product || product.productName || '', specificationId: product.specificationId || '', specification: product.specification || '', quantity: product.quantity || 0, unit: product.unit || '', unitPrice: product.unitPrice || 0, amount: product.amount || 0 })) : [] form.products = row.products ? row.products.map(product => { const productName = product.product || product.productName || '' // 优先用 productId;如果只有名称,尝试反查 id 以便树选择器反显 const resolvedId = product.productId ? Number(product.productId) : findNodeIdByLabel(productOptions.value, productName) || '' return { productId: resolvedId, product: productName, specificationId: product.specificationId || '', specification: product.specification || '', quantity: product.quantity || 0, unit: product.unit || '', unitPrice: product.unitPrice || 0, amount: product.amount || 0 } }) : [] form.subtotal = row.subtotal || 0 form.freight = row.freight || 0 form.otherFee = row.otherFee || 0 form.discountRate = row.discountRate || 0 form.discountAmount = row.discountAmount || 0 form.totalAmount = row.totalAmount || 0 // 反显审批人 if (row.approveUserIds) { const userIds = row.approveUserIds.split(',') approverNodes.value = userIds.map((userId, idx) => ({ id: idx + 1, userId: parseInt(userId.trim()) })) nextApproverId = userIds.length + 1 } else { approverNodes.value = [{ id: 1, userId: null }] nextApproverId = 2 } // 加载用户列表 let userLists = await userListNoPage(); userList.value = (userLists.data || []).map(item => ({ userId: item.userId, nickName: item.nickName || '', userName: item.userName || '' })); dialogVisible.value = true } @@ -596,6 +750,22 @@ return } // 审批人必填校验 const hasEmptyApprover = approverNodes.value.some(node => !node.userId) if (hasEmptyApprover) { ElMessage.error('请为所有审批节点选择审批人!') return } // 收集所有节点的审批人id form.approveUserIds = approverNodes.value.map(node => node.userId).join(',') // 计算所有产品的单价总和 form.totalAmount = form.products.reduce((sum, product) => { const price = Number(product.unitPrice) || 0 return sum + price }, 0) if (isEdit.value) { // 编辑 const index = quotationList.value.findIndex(item => item.id === editId.value) @@ -608,30 +778,16 @@ handleSearch() } }) // quotationList.value[index] = { ...form, id: editId.value } // ElMessage.success('编辑成功') } } else { // 新增 // const newId = Math.max(...quotationList.value.map(item => item.id)) + 1 form.quotationNo = `QT${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(new Date().getDate()).padStart(2, '0')}` addQuotation(form).then(res=>{ // console.log(res) if(res.code===200){ ElMessage.success('新增成功') dialogVisible.value = false handleSearch() } }) // quotationList.value.push({ // ...form, // // id: newId, // quotationNo: quotationNo // }) // pagination.total++ // ElMessage.success('新增成功') } } @@ -644,7 +800,7 @@ } const handleSearch = ()=>{ const params = { page:pagination, ...pagination, ...searchForm } getQuotationList(params).then(res=>{ @@ -660,6 +816,8 @@ validDate: item.validDate || '', paymentMethod: item.paymentMethod || '', status: item.status || '草稿', // 审批人(用于编辑时反显) approveUserIds: item.approveUserIds || '', remark: item.remark || '', products: item.products ? item.products.map(product => ({ productId: product.productId || '', @@ -696,27 +854,182 @@ }) </script> <style scoped> <style scoped lang="scss"> .search-row { margin-bottom: 20px; } .quotation-form-container { padding: 10px 0; max-height: calc(100vh - 200px); overflow-y: auto; &::-webkit-scrollbar { width: 6px; height: 6px; } &::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 3px; &:hover { background: #a8a8a8; } } } .quotation-form { .el-form-item { margin-bottom: 22px; } } .form-card { margin-bottom: 20px; margin-bottom: 24px; border-radius: 8px; transition: all 0.3s ease; &:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important; } :deep(.el-card__header) { padding: 16px 20px; background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%); border-bottom: 1px solid #ebeef5; } :deep(.el-card__body) { padding: 20px; } } .card-title { font-weight: bold; color: #303133; } .card-header { .card-header-wrapper { display: flex; justify-content: space-between; align-items: center; gap: 8px; .card-icon { font-size: 18px; color: #409eff; } .card-title { font-weight: 600; font-size: 16px; color: #303133; flex: 1; } .header-btn { margin-left: auto; } } .form-content { padding: 8px 0; } .approver-nodes-container { display: flex; flex-wrap: wrap; gap: 24px; padding: 12px 0; } .approver-node-item { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 16px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e4e7ed; transition: all 0.3s ease; min-width: 180px; &:hover { border-color: #409eff; background: #f0f7ff; box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1); } } .approver-node-label { display: flex; align-items: center; gap: 8px; font-size: 14px; color: #606266; .node-step { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; background: #409eff; color: #fff; border-radius: 50%; font-size: 12px; font-weight: 600; } .node-text { font-weight: 500; } .arrow-icon { color: #909399; font-size: 14px; } } .approver-select { width: 100%; min-width: 150px; } .remove-btn { margin-top: 4px; } .product-table { :deep(.el-table__header) { background-color: #f5f7fa; th { background-color: #f5f7fa !important; color: #606266; font-weight: 600; } } :deep(.el-table__row) { &:hover { background-color: #f5f7fa; } } :deep(.el-table__cell) { padding: 12px 0; } } .dialog-footer { text-align: right; } // 响应式优化 @media (max-width: 1200px) { .approver-nodes-container { gap: 16px; } .approver-node-item { min-width: 160px; } } </style>