FEATURED · 精选文章

基于Vue与Excel的前端自动化报价工作台开发实践

发布时间 / 2026/8/24 2:45:19
来源 / 创域科博编辑部
栏目 / 资讯中心
基于Vue与Excel的前端自动化报价工作台开发实践 在实际报价业务中很多团队还在依赖手工查表、复制粘贴、手动计算。这不仅效率低下容易出错而且当报价规则或成本结构发生变化时更新所有表格和公式的工作量巨大。一个能够根据预设规则自动计算、动态调整、并清晰展示的“AI报价工作台”可以显著提升报价的准确性和响应速度。这里的“AI”并非特指需要训练大模型更多是指通过程序化、自动化Automated Intelligence的方式将业务规则如阶梯价格、折扣策略、税费计算固化为代码逻辑并提供一个友好的前端界面进行交互。本文将手把手带你搭建这样一个工作台的核心部分一个能够读取外部数据如Excel、执行复杂计算逻辑、并实时反馈结果的前端应用。我们将聚焦于技术实现路径涵盖前端界面开发、Excel数据动态集成、核心计算函数编写以及如何构建一个清晰、可维护的项目结构。无论你是前端开发者希望深入业务逻辑集成还是业务人员想了解自动化报价的技术可行性本文都将提供一个从零到一的可复现实践指南。1. 理解报价工作台的核心架构与技术选型在动手写代码之前我们需要明确这个“AI报价工作台”要解决的核心问题以及技术上的实现路径。它不是一个简单的计算器而是一个将数据、规则、界面分离的系统。1.1 核心组件与数据流一个典型的报价工作台包含以下几个部分数据源存储产品基础信息、成本价、折扣规则等。最常见的形式是Excel/CSV文件也可以是数据库。业务规则引擎这是“AI”或自动化的核心。它定义了如何根据输入如产品型号、数量、客户等级查询数据源并应用复杂的计算规则如满减、阶梯价、组合优惠。用户界面供用户输入查询条件、查看报价明细和最终结果的交互界面。计算与展示层连接界面和规则引擎触发计算并实时更新显示结果。数据流通常为用户在界面选择或输入参数 - 前端将参数发送给计算函数 - 计算函数读取或请求数据 - 应用业务规则 - 返回结构化的计算结果 - 前端渲染结果。1.2 前端技术选型与考量对于这个工作台前端需要承担界面渲染和本地计算的双重任务。技术选型需考虑渲染与交互需要动态表单、表格展示、实时计算。现代前端框架如 Vue、React 或 Angular 都非常适合。数据处理需要高效读取和解析外部数据如Excel。浏览器端有成熟的库支持。计算逻辑JavaScript/TypeScript 足以处理大部分业务计算。复杂规则可能需要设计特定的算法。考虑到普及度和易上手性本文将使用Vue 3组合式 API 和Vite构建工具作为演示框架。计算逻辑将用纯 TypeScript 编写确保类型安全和逻辑清晰。1.3 为什么选择在浏览器端处理Excel将Excel处理逻辑放在前端浏览器有其独特优势离线可用用户可以在不连接服务器的情况下使用工作台数据隐私性也更好。即时反馈计算无需网络往返体验流畅。降低后端负载复杂的计算规则迭代时无需频繁部署后端服务。但这也带来了挑战需要处理文件上传、解析并且Excel文件较大或计算非常复杂时可能影响页面性能。对于企业级应用更常见的架构是“前端界面 后端API 独立数据库”前端负责交互后端负责规则运算和数据持久化。本文为演示完整性将重点展示前端主导的模式并在最后探讨如何演进为前后端分离架构。2. 环境准备与项目初始化我们将创建一个标准的 Vue 3 TypeScript Vite 项目并集成必要的依赖库。2.1 开发环境清单确保你的开发环境包含以下工具工具推荐版本作用验证命令Node.js18.x 或 20.x (LTS)JavaScript 运行时环境node -vnpm / yarn / pnpm最新稳定版包管理工具npm -v或yarn -v代码编辑器VS Code代码编写与调试-浏览器Chrome / Edge (最新版)运行和调试应用-2.2 创建Vue项目并安装核心依赖打开终端执行以下命令创建项目# 使用 npm 创建 Vite 项目选择 Vue 和 TypeScript 模板 npm create vitelatest ai-quotation-desk -- --template vue-ts # 进入项目目录 cd ai-quotation-desk # 安装项目依赖 npm install # 安装处理 Excel 和 UI 组件的额外依赖 npm install xlsx lucide-vue-nextxlsx: 一个功能强大的库用于在浏览器中读取、解析和写入 Excel 文件。它是实现动态读取报价表数据的关键。lucide-vue-next: 一套精美的图标库用于美化界面。你也可以选择其他 UI 库如 Element Plus、Ant Design Vue 等。2.3 项目结构设计清晰的项目结构是维护性的基础。创建以下目录和文件ai-quotation-desk/ ├── public/ # 静态资源 ├── src/ │ ├── assets/ # 图片、样式等资源 │ ├── components/ # 可复用组件 │ │ ├── ui/ # 基础UI组件 (如按钮、输入框) │ │ └── quotation/ # 报价业务组件 │ ├── composables/ # Vue 组合式函数 (核心逻辑) │ │ ├── useExcelParser.ts # Excel解析逻辑 │ │ └── useQuotationCalculator.ts # 报价计算引擎 │ ├── data/ # 模拟数据或静态配置 │ │ └── sample-price-list.xlsx # 示例Excel报价表 │ ├── types/ # TypeScript 类型定义 │ │ └── index.ts │ ├── views/ # 页面组件 │ │ └── QuotationView.vue # 报价工作台主页面 │ ├── App.vue │ ├── main.ts │ └── style.css ├── index.html ├── package.json ├── tsconfig.json └── vite.config.ts这个结构将业务逻辑 (composables)、数据类型 (types)、UI组件 (components) 和视图 (views) 分离符合关注点分离原则。3. 构建报价计算引擎与数据解析层这是工作台的“大脑”负责加载规则、执行计算。我们先从数据类型和Excel解析开始。3.1 定义核心数据类型在src/types/index.ts中定义报价业务涉及的核心类型// 产品基础信息对应Excel中的一行 export interface ProductItem { id: string; // 产品ID sku: string; // 产品SKU name: string; // 产品名称 category: string; // 产品类别 basePrice: number; // 基础单价 cost: number; // 成本价 unit: string; // 单位 } // 折扣规则 export interface DiscountRule { id: string; name: string; type: percentage | fixed | step; // 折扣类型百分比、固定金额、阶梯 condition: { minQuantity?: number; // 最小数量 maxQuantity?: number; // 最大数量 customerTier?: string; // 客户等级 [key: string]: any; // 其他条件 }; value: number; // 折扣值百分比如0.1代表10%固定金额如50 } // 报价请求参数 export interface QuotationRequest { productSku: string; quantity: number; customerTier?: string; extraParams?: Recordstring, any; } // 报价结果明细 export interface QuotationResult { request: QuotationRequest; product: ProductItem | null; unitPrice: number; // 应用规则后的单价 subtotal: number; // 数量 * 单价 discountAmount: number; // 折扣金额 taxAmount: number; // 税额 (示例) total: number; // 总计 appliedRules: string[]; // 应用到的规则ID或名称 calculationSteps: string[]; // 计算步骤用于展示或调试 }3.2 实现Excel文件解析器在src/composables/useExcelParser.ts中创建解析Excel的组合式函数import { ref } from vue; import * as XLSX from xlsx; import type { ProductItem } from /types; export function useExcelParser() { const productList refProductItem[]([]); const loading ref(false); const error refstring | null(null); // 解析上传的Excel文件 const parseExcelFile (file: File): Promisevoid { return new Promise((resolve, reject) { loading.value true; error.value null; const reader new FileReader(); reader.onload (e) { try { const data e.target?.result; const workbook XLSX.read(data, { type: binary }); // 假设数据在第一个工作表Sheet1 const firstSheetName workbook.SheetNames[0]; const worksheet workbook.Sheets[firstSheetName]; // 将工作表转换为JSON数组header行默认为第一行 const jsonData: any[] XLSX.utils.sheet_to_json(worksheet); // 映射Excel列到ProductItem类型 // 注意这里需要根据你实际Excel的列名调整映射关系 const mappedProducts: ProductItem[] jsonData.map((row, index) ({ id: row[产品ID] || prod-${index 1}, sku: row[SKU] || row[产品编码] || , name: row[产品名称] || , category: row[产品类别] || , basePrice: parseFloat(row[基础单价]) || 0, cost: parseFloat(row[成本价]) || 0, unit: row[单位] || 个, })).filter(item item.sku); // 过滤掉SKU为空的行 productList.value mappedProducts; console.log(成功加载 ${mappedProducts.length} 条产品数据); resolve(); } catch (err) { error.value 解析Excel文件失败: ${err}; console.error(Excel解析错误:, err); reject(err); } finally { loading.value false; } }; reader.onerror () { error.value 读取文件失败; loading.value false; reject(new Error(File read failed)); }; reader.readAsBinaryString(file); }); }; // 根据SKU查找产品 const findProductBySku (sku: string): ProductItem | undefined { return productList.value.find(product product.sku sku); }; return { productList, loading, error, parseExcelFile, findProductBySku, }; }关键点xlsx库的sheet_to_json方法将Excel工作表直接转为JSON对象数组。映射逻辑 (row[列名]) 必须与你的Excel表头完全一致。生产环境中需要增加更严格的列名校验和数据类型转换容错。3.3 实现报价计算引擎在src/composables/useQuotationCalculator.ts中构建核心计算逻辑import { ref, computed } from vue; import type { ProductItem, DiscountRule, QuotationRequest, QuotationResult } from /types; // 模拟的折扣规则库实际可从Excel或API加载 const mockDiscountRules: DiscountRule[] [ { id: rule1, name: 普通客户9折, type: percentage, condition: { customerTier: standard }, value: 0.1 }, { id: rule2, name: VIP客户85折, type: percentage, condition: { customerTier: vip }, value: 0.15 }, { id: rule3, name: 批量采购满100件减200, type: fixed, condition: { minQuantity: 100 }, value: 200 }, { id: rule4, name: 阶梯价1-10件原价11-50件95折51件9折, type: step, condition: {}, value: 0 }, // value在函数内处理 ]; export function useQuotationCalculator(getProductBySku: (sku: string) ProductItem | undefined) { const calculationHistory refQuotationResult[]([]); // 核心计算函数 const calculateQuotation (request: QuotationRequest): QuotationResult { const steps: string[] []; steps.push(开始计算报价产品SKU: ${request.productSku}, 数量: ${request.quantity}); // 1. 查找产品 const product getProductBySku(request.productSku); if (!product) { throw new Error(未找到SKU为 ${request.productSku} 的产品); } steps.push(找到产品: ${product.name}基础单价: ${product.basePrice}); let unitPrice product.basePrice; let discountAmount 0; const appliedRules: string[] []; // 2. 应用折扣规则按优先级 for (const rule of mockDiscountRules) { if (isRuleApplicable(rule, request)) { appliedRules.push(rule.name); const discount applyDiscount(rule, unitPrice, request.quantity); steps.push(应用规则【${rule.name}】, 折扣影响: ${discount.type fixed ? 减${discount.value}元 : 单价乘以${1 - discount.value}}); if (discount.type percentage) { unitPrice unitPrice * (1 - discount.value); } else if (discount.type fixed) { // 固定折扣平摊到单价仅为计算展示实际总额扣除 discountAmount discount.value; } else if (discount.type step) { unitPrice calculateStepPrice(request.quantity, product.basePrice); } } } // 3. 计算各项金额 const subtotal unitPrice * request.quantity; // 固定折扣从总额中扣除 const finalSubtotal subtotal - discountAmount; // 示例计算13%的增值税 const taxRate 0.13; const taxAmount finalSubtotal * taxRate; const total finalSubtotal taxAmount; steps.push(计算小计: ${unitPrice} * ${request.quantity} ${subtotal}); if (discountAmount 0) steps.push(应用固定折扣: -${discountAmount}); steps.push(计算税额 (${taxRate*100}%): ${taxAmount.toFixed(2)}); steps.push(总计: ${total.toFixed(2)}); const result: QuotationResult { request, product, unitPrice: parseFloat(unitPrice.toFixed(2)), subtotal: parseFloat(subtotal.toFixed(2)), discountAmount, taxAmount: parseFloat(taxAmount.toFixed(2)), total: parseFloat(total.toFixed(2)), appliedRules, calculationSteps: steps, }; // 存入历史 calculationHistory.value.unshift(result); return result; }; // 规则适用性判断 const isRuleApplicable (rule: DiscountRule, request: QuotationRequest): boolean { const cond rule.condition; if (cond.customerTier cond.customerTier ! request.customerTier) return false; if (cond.minQuantity request.quantity cond.minQuantity) return false; if (cond.maxQuantity request.quantity cond.maxQuantity) return false; // 可以扩展更多条件判断 return true; }; // 应用折扣计算 const applyDiscount (rule: DiscountRule, currentPrice: number, quantity: number) { if (rule.type step) { // 阶梯价处理 return { type: step as const, value: 0 }; } return { type: rule.type, value: rule.value }; }; // 阶梯价格计算函数 const calculateStepPrice (quantity: number, basePrice: number): number { if (quantity 10) return basePrice; if (quantity 50) return basePrice * 0.95; return basePrice * 0.9; // 51件以上 }; // 清空历史 const clearHistory () { calculationHistory.value []; }; return { calculationHistory, calculateQuotation, clearHistory, }; }设计思路计算引擎被设计为纯函数除历史记录外接收请求参数和产品数据返回结构化的结果。规则与应用逻辑分离便于后续从文件或API加载规则。calculationSteps数组对于调试和向用户展示计算过程非常有用。4. 搭建用户界面与实现交互逻辑有了计算引擎和数据解析器现在需要将它们与用户界面连接起来。4.1 构建主页面组件创建src/views/QuotationView.vuetemplate div classquotation-view h1AI 智能报价工作台/h1 p上传产品价格表输入参数自动计算精准报价。/p div classmain-layout !-- 左侧配置与输入区 -- div classleft-panel section classupload-section h2UploadIcon / 1. 上传价格表 (Excel)/h2 input typefile accept.xlsx, .xls, .csv changehandleFileUpload :disabledexcelParser.loading / div v-ifexcelParser.loading正在解析文件.../div div v-ifexcelParser.error classerror-message{{ excelParser.error }}/div div v-ifexcelParser.productList.length 0 classsuccess-message 已加载 {{ excelParser.productList.length }} 条产品记录。 select v-modelselectedProductSku option value-- 选择产品 --/option option v-forproduct in excelParser.productList :keyproduct.sku :valueproduct.sku {{ product.sku }} - {{ product.name }} (¥{{ product.basePrice }}) /option /select /div /section section classinput-section h2CalculatorIcon / 2. 输入报价参数/h2 div classinput-group label forquantity采购数量/label input idquantity typenumber v-model.numberquantity min1 :disabled!selectedProductSku / /div div classinput-group label forcustomerTier客户等级/label select idcustomerTier v-modelcustomerTier :disabled!selectedProductSku option valuestandard普通客户/option option valuevipVIP客户/option option valuevvipVVIP客户/option /select /div button clickhandleCalculate :disabled!canCalculate开始计算/button /section section classhistory-section v-ifcalculator.calculationHistory.length 0 h2HistoryIcon / 历史计算/h2 button clickcalculator.clearHistory()清空历史/button ul li v-for(item, index) in calculator.calculationHistory :keyindex clickshowDetail(item) {{ item.product?.name }} x {{ item.request.quantity }} ¥{{ item.total }} /li /ul /section /div !-- 右侧结果展示区 -- div classright-panel section classresult-section v-ifcurrentResult h2ReceiptIcon / 报价结果明细/h2 div classresult-card div classresult-row span产品名称/spanstrong{{ currentResult.product?.name }}/strong /div div classresult-row spanSKU/spanspan{{ currentResult.request.productSku }}/span /div div classresult-row span数量/spanspan{{ currentResult.request.quantity }} {{ currentResult.product?.unit }}/span /div div classresult-row span基础单价/spanspan¥{{ currentResult.product?.basePrice }}/span /div div classresult-row span成交单价/spanspan¥{{ currentResult.unitPrice }}/span /div div classresult-row highlight span小计/spanspan¥{{ currentResult.subtotal }}/span /div div classresult-row v-ifcurrentResult.discountAmount 0 span折扣金额/spanspan classdiscount-¥{{ currentResult.discountAmount }}/span /div div classresult-row span税额 (13%)/spanspan¥{{ currentResult.taxAmount }}/span /div div classresult-row total span总计/spanspan¥{{ currentResult.total }}/span /div div classapplied-rules v-ifcurrentResult.appliedRules.length 0 h4应用的优惠规则/h4 ul li v-forrule in currentResult.appliedRules :keyrule{{ rule }}/li /ul /div /div h3计算步骤/h3 ol classsteps li v-for(step, idx) in currentResult.calculationSteps :keyidx{{ step }}/li /ol /section div v-else classplaceholder p请先上传价格表并输入参数进行计算。/p /div /div /div /div /template script setup langts import { ref, computed } from vue; import { UploadIcon, CalculatorIcon, HistoryIcon, ReceiptIcon } from lucide-vue-next; import { useExcelParser } from /composables/useExcelParser; import { useQuotationCalculator } from /composables/useQuotationCalculator; import type { QuotationRequest, QuotationResult } from /types; const excelParser useExcelParser(); const selectedProductSku ref(); const quantity ref(1); const customerTier ref(standard); const currentResult refQuotationResult | null(null); // 初始化计算引擎传入产品查询函数 const calculator useQuotationCalculator(excelParser.findProductBySku); const canCalculate computed(() { return selectedProductSku.value quantity.value 0; }); const handleFileUpload async (event: Event) { const target event.target as HTMLInputElement; const file target.files?.[0]; if (!file) return; try { await excelParser.parseExcelFile(file); selectedProductSku.value ; // 重置选择 } catch (err) { console.error(文件处理失败:, err); } }; const handleCalculate () { if (!canCalculate.value) return; const request: QuotationRequest { productSku: selectedProductSku.value, quantity: quantity.value, customerTier: customerTier.value, }; try { const result calculator.calculateQuotation(request); currentResult.value result; } catch (error: any) { alert(计算失败: ${error.message}); } }; const showDetail (result: QuotationResult) { currentResult.value result; }; /script style scoped /* 样式代码省略可根据需要添加布局、颜色、响应式等样式 */ .main-layout { display: flex; gap: 2rem; } .left-panel, .right-panel { flex: 1; } .input-group { margin-bottom: 1rem; } .result-card { border: 1px solid #ccc; padding: 1rem; border-radius: 8px; } .result-row { display: flex; justify-content: space-between; margin-bottom: 0.5rem; } .total { font-size: 1.2em; font-weight: bold; border-top: 2px solid #333; padding-top: 0.5rem; } .discount { color: #f00; } .steps { text-align: left; } .error-message { color: #f00; } .success-message { color: #090; } /style4.2 更新主应用入口修改src/App.vue引入我们的报价页面template div idapp QuotationView / /div /template script setup langts import QuotationView from ./views/QuotationView.vue; /script style /* 全局样式 */ body { font-family: sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; } /style5. 运行验证与功能测试完成编码后我们需要验证整个工作流是否畅通。5.1 准备测试数据在src/data/目录下创建一个名为sample-price-list.xlsx的Excel文件。你可以使用 Microsoft Excel、WPS 或在线工具创建包含以下数据表头为第一行产品IDSKU产品名称产品类别基础单价成本价单位P001NBK-1001笔记本电脑电子产品6500.005200.00台P002MBL-2001智能手机电子产品3999.003200.00部P003DSK-3001办公桌家具1200.00800.00张5.2 启动开发服务器并测试在项目根目录运行npm run dev访问控制台输出的本地地址通常是http://localhost:5173。按照界面步骤操作上传Excel点击上传按钮选择刚创建的sample-price-list.xlsx文件。控制台应打印“成功加载 3 条产品数据”界面下拉框出现产品选项。选择产品从下拉框中选择一个产品如“NBK-1001 - 笔记本电脑”。输入参数输入数量如 20选择客户等级如“VIP客户”。计算点击“开始计算”按钮。查看结果右侧面板应展示详细的报价单包括应用VIP折扣85折后的单价、小计、折扣、税额和总计。下方应列出计算步骤。5.3 验证核心计算逻辑根据我们的模拟规则VIP客户85折数量20未触发阶梯和满减计算过程应为基础单价6500VIP折扣后单价6500 * (1 - 0.15) 5525小计5525 * 20 110,500税额110,500 * 0.13 14,365总计110,500 14,365 124,865检查界面显示的总计是否与手动计算一致。可以尝试修改数量为 120触发“满100件减200”的固定折扣规则观察折扣金额是否正确扣除。6. 常见问题排查与优化建议在实际开发和使用中你可能会遇到以下问题。6.1 问题排查清单问题现象可能原因检查与解决方式上传Excel后无产品列表1. Excel列名与代码中映射的键不匹配。2. 文件读取或解析失败。1. 打开浏览器开发者工具F12的“控制台”(Console)标签页查看错误信息。2. 在useExcelParser.ts的parseExcelFile函数内console.log(jsonData)查看原始解析出的JSON结构确保列名一致。3. 检查Excel文件格式确保不是加密或损坏的文件。点击计算无反应或报错“未找到产品”1. 产品SKU选择为空或未正确绑定。2.findProductBySku函数逻辑有误。1. 检查selectedProductSku这个响应式变量是否正确绑定到下拉框 (v-model)。2. 在计算函数开始处console.log(request)确认参数正确。3. 检查findProductBySku函数确认它从正确的数据源 (productList.value) 中查找。计算结果与预期不符1. 折扣规则判断逻辑 (isRuleApplicable) 有误。2. 计算函数 (applyDiscount,calculateStepPrice) 逻辑错误。3. 数据类型问题如字符串未转数字。1. 在calculateQuotation函数中逐步console.log每个阶段的值如unitPrice在应用每个规则后的变化。2. 仔细核对mockDiscountRules中的条件和值。3. 确保从Excel解析出的basePrice是number类型而非string。界面样式错乱或布局问题1. CSS样式未正确加载或作用域问题。2. 响应式布局未适配。1. 检查style scoped是否影响子组件。必要时使用深度选择器 (:deep())。2. 使用浏览器开发者工具的“元素”(Elements)和“样式”(Styles)面板检查元素实际应用的CSS。6.2 性能与体验优化建议大文件处理如果Excel文件很大超过数万行前端解析会阻塞主线程。可以使用Web Worker将解析任务放到后台线程或要求后端处理。数据持久化每次刷新页面上传的数据会丢失。可以考虑使用localStorage或IndexedDB在浏览器端暂存解析后的产品数据。规则管理当前折扣规则硬编码在代码中。理想情况下规则也应支持从Excel或配置文件中动态加载形成一个“数据产品表 规则折扣表”的双驱动模式。计算缓存对于相同的请求参数可以缓存计算结果避免重复计算。错误边界增加更完善的错误处理如文件类型校验、数据格式校验、网络错误提示等。6.3 向生产环境演进当前实现是一个完整的前端单机版。要用于团队协作或更复杂的业务需要考虑以下演进方向前后端分离前端 (Vue/React)专注于界面渲染、用户交互和请求发送。后端 (Node.js/Java/Python)提供RESTful API。POST /api/upload接收Excel文件解析后存入数据库。GET /api/products获取产品列表。POST /api/calculate接收报价请求执行所有业务规则计算返回结果。数据库存储产品主数据、折扣规则、历史报价单、用户信息等。规则引擎抽象将折扣、税费、运费等计算规则抽象为可配置的规则链或脚本如使用JSON配置、DSL或嵌入轻量级脚本引擎实现无需发版即可修改业务规则。权限与审计增加用户登录、角色权限控制并记录所有的报价操作日志满足审计要求。部署与运维使用Docker容器化配合Nginx等Web服务器部署前端后端服务独立部署并考虑负载均衡。通过以上步骤你已经成功搭建了一个具备核心自动计算能力的前端“AI报价工作台”原型。它演示了如何将传统的Excel手工查表流程转化为一个可交互、可扩展的Web应用。你可以在此基础上根据实际业务需求深化规则引擎、完善UI/UX、或将其改造成前后端分离的完整系统。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻