ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

纯HTML电子发票收据凭证界面开发:从数字转换到自动合计

纯HTML电子发票收据凭证界面开发:从数字转换到自动合计 简介这份HTML制作的电子发票收据凭证界面项目面向Web前端初中级学习者和有快速开票需求的小微财务场景尤其适合前端课程设计、财务信息化入门或日常凭证打印工具开发解决手工填写收据易出错、合计计算繁琐等问题。项目以HTML表格、表单控件为基础结合JavaScript动态交互实现金额录入、中文数字识别、添加删除明细行以及合计金额自动计算允许多项明细实时汇总核对页面结构清晰交互直观。压缩包共3个文件包括一张效果图截图、一个基础版收据源码压缩包和一个改进版HTML页面整体仅81KB资源非常轻量下载后即可参照效果图快速运行也便于按需调整样式和逻辑。已有1534人学习浏览。通过对照基础版与改进版代码能够清晰看到功能优化点同时掌握前端表单校验、中文数字转阿拉伯数字、表格行操作、事件监听与金额累加等常用技巧可作为课程设计、结课作业或实际项目中的直接参考具备较好的学习与复用价值。1. 电子发票收据凭证界面用纯HTML做先解决“算钱”和“防涂改”开一张收据凭证先把品名、数量、单价列出来按一遍计算器得出合计再把合计用小写写一遍、中文大写写一遍核对无误后才能交给对方。这套动作在纸质单据时代是财务的基本功放到今天Excel模板能省一部分事但模板在发给别人时经常格式错乱、公式被误删换了电脑打开样子就不对了。纯HTML的电子发票收据凭证界面绕开了这些问题一个文件双击就在浏览器里打开不用装运行时不需要网络。填写数字、中文数字与阿拉伯数字互转、自动计算合计、金额大写全部在本地完成打印版式和屏幕上看到的一致。它适合小商户开收款收据、企业内部做报销凭证、个人租房写收条也适合前端练习时拿来做练手项目——重头戏正好在数据计算和表单交互上不涉及框架和后端。下面把这套做法完整拆开先用HTML把凭证的头部、明细表、尾栏搭出来再用JavaScript实现数字转换和合计计算最后处理打印和zip打包分发时绕不开的坑。2. 搭建收据凭证界面的HTML骨架与表格结构2.1 收据界面的字段模型头部、明细、尾栏三段放什么收据凭证界面的信息结构和普通表单最大的不同在于它要兼顾“录入”和“归档”两种用途。录入时操作员要快速找到对应字段归档时字段顺序必须符合纸面单据的阅读习惯不然以后翻查对账很费劲。常见做法是把页面分成三段头部区收据编号、开票日期、付款方名称、收款方名称明细区序号、项目名称、数量、单位、单价、金额尾栏区合计小写、合计大写、备注、经办人头部和尾栏用div容器承载明细区用table承载。不建议整张凭证都用table布局table在跨列合并、底部合计对齐上有天然优势但在控制间距、做小屏适配时很受限和div混用才是html网页制作里更顺手的组合。区域字段输入类型计算方式头部收据编号text手工输入头部开票日期date默认当天头部付款方text手工输入明细项目名称text手工输入明细数量number手工输入明细单价number手工输入明细金额文本展示数量 × 单价尾栏合计小写文本展示各行金额累加尾栏合计大写文本展示由合计自动转换这个字段模型的好处是每个区域边界清晰后续做自动计算和中文数字转换时只需要认准每个input的class或data属性不需要维护复杂的id列表。字段要不要加必填标识看使用场景——只是自己记录空几项没关系如果作为收款凭证交给对方编号和金额不能空。2.2 明细行的HTML结构可编辑单元格与行模板明细区是整张凭证交互最密集的区域。每行包含项目名称、数量、单位、单价、金额五个单元格其中金额是计算列。为什么把金额做成只读展示而不是让用户自己填一是减少重复输入二是防止手工算错后凭证上出现前后矛盾的数字。table idinvoiceTable thead tr th stylewidth:40px序号/th th项目名称/th th stylewidth:70px数量/th th stylewidth:60px单位/th th stylewidth:100px单价元/th th stylewidth:110px金额元/th /tr /thead tbody idinvoiceBody tr classitem-row td classrow-index1/td tdinput typetext classitem-name placeholder商品或服务名称/td tdinput typenumber classitem-qty min0 step1 value1/td tdinput typetext classitem-unit value项/td tdinput typenumber classitem-price min0 step0.01 value0/td td classitem-amount0.00/td /tr /tbody /table button idaddRowBtn typebutton添加一行/button代码说明序号列是纯文本td行号由JavaScript统一维护不在HTML里写死这样删除中间行后不需要手工改序号。数量和单价用typenumber浏览器自带步进按钮移动端还会自动弹出数字键盘。金额列是td加classitem-amount用textContent展示计算结果用户无法直接篡改。一个容易被忽略的细节单价和金额输入框里不要带货币符号。界面上能看见的单位只有列头的“元”输入框只放纯数字。这样后面做中文数字转换、金额大写拼接时拿到的input.value就是干净字符串不用正则先把“”或“元”剥掉。2.3 CSS排版把表单做成一张凭证纸凭证界面的视觉目标不是后台管理系统那种大卡片、大留白而是贴近纸质收据的密实观感白底、细边框、行距紧凑、金额列右对齐。这样打印出来更像一张正规单据单位面积内也能放下更多明细行。.invoice-wrapper { width: 720px; max-width: 100%; margin: 0 auto; padding: 28px 32px; border: 1px solid #ccc; background: #fff; font-family: Microsoft YaHei, PingFang SC, sans-serif; } #invoiceTable { width: 100%; border-collapse: collapse; font-size: 14px; } #invoiceTable th, #invoiceTable td { border: 1px solid #999; padding: 5px 8px; } #invoiceTable .col-amount { text-align: right; font-variant-numeric: tabular-nums; } #invoiceTable input { width: 100%; border: none; outline: none; background: transparent; font-size: 14px; box-sizing: border-box; }CSS说明最外层把凭证在页面里居中固定720px宽超出部分用max-width: 100%兜底手机浏览器上不至于出现横向滚动条。表格用border-collapse: collapse把单元格边框合并成单线视觉上更接近印刷票据。输入框去掉边框和背景和表格融为一体用户看到的是可以直接在表格里填写的单据而不是一堆输入框排成的表单。提示表格里的input不要设置大padding否则行高被撑开整张凭证会变得松散。数量、单价、金额这几列宽度固定名称列吃掉剩余宽度。金额列加tabular-nums让数字按等宽方式渲染小数点在竖直方向上对齐这是账本类界面常用的细节处理。3. 数字与中文数字互转收据自动填写的核心逻辑3.1 为什么收据要做中文数字输入与输出普通网页表单里用户填阿拉伯数字是天经地义的事但收据凭证这个场景有特殊性。财务和银行体系中金额要同时以小写数字和中文大写两种形态出现大写的作用是防篡改壹、贰、叁这些字笔画复杂涂改痕迹很容易辨认。所以收据界面的中文数字需求分成两条线。一条线是阿拉伯数字自动转中文大写用于大写合计栏展示另一条线是接收用户输入的中文数字比如有人习惯直接输入“一百二十”而不是120系统要能解析成数值参与计算。两条线都做好凭证界面才谈得上实用。场景输入形态处理逻辑输出合计大写阿拉伯数字整数分段、角分规约壹仟贰佰叁拾肆元伍角陆分用户输入中文小写或大写权值累加解析120 或 3.53.2 阿拉伯数字转中文数字按万节单位递推中文数字的单位结构是“万、亿、兆”分层每层内部再分“千百十”。一个整数转中文正确做法是每四位切成一段段内按千百十位处理段间拼上万或亿。如果直接写一长串if-else判断十位百位千位代码会乱到没法维护。function numberToChineseUpper(num) { const digits [零, 壹, 贰, 叁, 肆, 伍, 陆, 柒, 捌, 玖]; const units [, 拾, 佰, 仟]; const bigUnits [, 万, 亿, 兆]; if (num 0) return 零; const sections []; while (num 0) { sections.push(num % 10000); num Math.floor(num / 10000); } let result ; for (let i sections.length - 1; i 0; i--) { const section sections[i]; if (section 0) { if (result !result.endsWith(零)) result 零; continue; } let sectionStr ; let pendingZero false; for (let j 3; j 0; j--) { const d Math.floor(section / Math.pow(10, j)) % 10; if (d 0) { pendingZero true; } else { if (pendingZero sectionStr) sectionStr 零; pendingZero false; sectionStr digits[d] units[j]; } } if (sectionStr.endsWith(零)) sectionStr sectionStr.slice(0, -1); result sectionStr bigUnits[i]; } return result; }逻辑说明sections数组从低位开始每四位压一个元素进去循环时从高位往低位取出来。内层循环处理一段里的千百十个位pendingZero用来压缩连续零——1002要读成“壹仟零贰”而不是“壹仟零零贰”。每段结尾如果残留一个零比如1020会转成“壹仟零贰拾零”需要在拼接单位前把末尾的零去掉。这个函数输出的是大写中文数字。如果要显示小写中文数字把digits数组换成一、二、三那套字表即可。两个数组分开维护比做一个通用参数更直白收据场景里用到的地方也就两三处。3.3 中文数字解析回阿拉伯数字权值累加用户输入“一百二十”或“壹佰贰拾元整”界面要解析出120。解析算法的思路正好和转换相反从左到右扫描遇到数字字符先暂存遇到十、百、千时把暂存值乘以权值累加到当前段遇到万或亿时切段并累加到总数。function chineseToNumber(input) { const numMap { 零: 0, 一: 1, 二: 2, 两: 2, 三: 3, 四: 4, 五: 5, 六: 6, 七: 7, 八: 8, 九: 9, 壹: 1, 贰: 2, 叁: 3, 肆: 4, 伍: 5, 陆: 6, 柒: 7, 捌: 8, 玖: 9 }; const unitMap { 十: 10, 拾: 10, 百: 100, 佰: 100, 千: 1000, 仟: 1000 }; const sectionMap { 万: 10000, 亿: 100000000 }; let total 0; let section 0; let current 0; for (const ch of input) { if (ch in numMap) { current numMap[ch]; } else if (ch in unitMap) { if (current 0) current 1; section current * unitMap[ch]; current 0; } else if (ch in sectionMap) { if (current ! 0) section current; total section * sectionMap[ch]; section 0; current 0; } } total section current; return total; }参数说明numMap同时覆盖小写和大写数字用户从网页上复制粘贴过来的内容可能是任意形态。unitMap处理十百千sectionMap处理万和亿。解析“十二”时十前面没有数字current为0代码里把这种情况置为1否则会算成2。遇到“元”“整”“块”这类非数字字符直接跳过所以字符串里混着中文单位文字也能正确解析。这个解析函数对“一千二”这种口语式省略不处理按规则解析会得到1002而不是1200。收据输入框的使用场景里要求用户给出完整写法比过度宽容更稳妥解析出错时在界面上提示“无法识别的数字”就行。3.4 人民币金额大写角分与“整”字规则上面两节处理的是整数但收据合计通常有小数。人民币大写有专门规则整数部分到“元”为止角位和分位分别处理角分都为零时加“整”字防篡改只有分位有数字时角位必须用“零”占住写作“伍元零叁分”。function toRmbUpperCase(num) { const intPart Math.floor(num); const decPart Math.round((num - intPart) * 100); const jiao Math.floor(decPart / 10); const fen decPart % 10; let result numberToChineseUpper(intPart) 元; if (jiao 0 fen 0) { result numberToChineseUpper(jiao) 角 numberToChineseUpper(fen) 分; } else if (jiao 0) { result numberToChineseUpper(jiao) 角整; } else if (fen 0) { result 零 numberToChineseUpper(fen) 分; } else { result 整; } return result; }调用示例toRmbUpperCase(1234.5)返回“壹仟贰佰叁拾肆元伍角整”toRmbUpperCase(0.03)返回“零元零叁分”。Math.round((num - intPart) * 100)这一步先把小数放大到分再取整避开浮点误差。如果直接用(num - intPart) * 10在0.1这类数上会得到1.0000000000000002这种结果角位判断就会出错。提示金额大写只保留角和分两级厘位直接四舍五入掉。如果业务上有按厘计息的需求这段逻辑要单独改不能套用收据场景的两位小数规则。4. 自动计算合计从监听输入到金额回填4.1 事件委托触发合计重算收据界面要自动计算合计第一步是让每一行的金额随数量和单价实时变化。如果给每个输入框单独绑定监听动态添加新行时新节点不会自动带上监听需要二次绑定容易漏。事件委托是动态表格场景下更稳的方案。function round2(val) { return Math.round((val Number.EPSILON) * 100) / 100; } function recalcRow(row) { const qty parseFloat(row.querySelector(.item-qty).value); const price parseFloat(row.querySelector(.item-price).value); const qtySafe isNaN(qty) ? 0 : qty; const priceSafe isNaN(price) ? 0 : price; const amount round2(qtySafe * priceSafe); row.querySelector(.item-amount).textContent amount.toFixed(2); return amount; } function recalcAll() { const rows document.querySelectorAll(#invoiceBody .item-row); let total 0; rows.forEach(function(row) { total recalcRow(row); }); total round2(total); document.getElementById(totalLower).textContent total.toFixed(2); document.getElementById(totalUpper).textContent toRmbUpperCase(total); } document.getElementById(invoiceBody).addEventListener(input, function(e) { if (e.target.matches(.item-qty, .item-price)) { recalcAll(); } });逻辑说明监听器绑定在tbody上用户输入数量或单价时触发recalcAll。e.target.matches只认这两个class其他输入框比如项目名称输入时不会触发无意义的计算。recalcRow里的querySelector只在本行范围内查找行与行之间不会串。parseFloat解析空值会得到NaN用isNaN归零避免NaN直接显示在金额列。同步更新两个合计位置小写数字显示在尾栏的totalLower中文大写显示在totalUpper。如果界面上还想展示中文小写数字比如“合计一万二千三百四十五”再加一个字段调用numberToChineseUpper就行。4.2 金额精度处理与四舍五入JavaScript浮点数的运算误差是收据界面的隐藏地雷。0.1加0.2在控制台会输出0.30000000000000004数量乘单价的结果如果直接展示屏幕上可能冒出一长串小数。收据场景的处理方式可以归纳成一张表处理环节做法原因单行小计parseFloat输入值乘完用round2先把结果规约到两位小数合计累加每个小计最后再round2避免累加过程引入误差展示toFixed(2)保证字符串输出固定两位round2这段代码里Math.round((val Number.EPSILON) * 100) / 100是常规四舍五入写法Number.EPSILON用来修正1.005这种边界数——在IEEE754表示里1.005乘100等于100.49999999999999直接round会得到100而不是101加一个极小值把误差顶过去就对了。还有一类输入风险用户把单价填成三位小数比如10.999。按照收据规则应当先四舍五入到两位11.00再参与计算而不是直接乘数量。如果漏掉这步一张包含几十行的凭证最后合计会出现千分位误差。4.3 添加行、删除行与序号重排联动明细行的增删要考虑三个联动新行的输入值要清空、行号要按新顺序重排、合计要立即重算。添加行用cloneNode复制模板行比createElement逐字段创建省代码但克隆出来的旧值必须手动清理。document.getElementById(addRowBtn).addEventListener(click, function() { const tbody document.getElementById(invoiceBody); const template tbody.querySelector(.item-row); const newRow template.cloneNode(true); newRow.querySelector(.item-name).value ; newRow.querySelector(.item-qty).value 1; newRow.querySelector(.item-price).value 0; newRow.querySelector(.item-amount).textContent 0.00; tbody.appendChild(newRow); renumberRows(); recalcAll(); }); function renumberRows() { const rows document.querySelectorAll(#invoiceBody .item-row); rows.forEach(function(row, idx) { row.querySelector(.row-index).textContent idx 1; }); }补充说明删除行时给每行加一个删除按钮绑定click事件后用row.remove()删掉当前行再执行renumberRows和recalcAll。边界情况是当前只剩一行时删除按钮直接禁用否则表格会出现空tbody合计显示0填了一半的数据全没了。这里有个反直觉的顺序要求recalcAll要在renumberRows之后调用。虽然行号和计算没有直接依赖但保持“先调整结构、再更新数值”的固定顺序可以避免多次操作叠加时出现中间状态——用户双击添加按钮快速加两行的时候其中一次重算可能拿到旧的行号数组。5. 打印、单文件复用与zip交付的几个边界处理5.1 用media print把界面切成凭证纸凭证开完下一步通常是打印。浏览器默认打印会把按钮、输入框边框、页面背景全部打出来不能直接交付。在media print里隐藏所有操作按钮和辅助元素去掉输入框的边框和背景让填写内容的输入框以普通文本形态呈现在表格里。打印时要通过page规则设定纸张大小我一般用A5margin设10mm左右。凭证宽度不再受限720px改成100%铺满页面。这样窄表格在打印时左侧不会留下大块空白。5.2 单文件HTML与zip打包的分发体验zip包交付的核心是解压后双击就能用这要求HTML保持单文件形态CSS写在style标签里JavaScript写在script标签里图片全部内联为base64或SVG。避免使用相对路径加载外部资源用户一旦把文件挪了位置或者发到别人那里界面就残缺了。如果开发阶段想用多文件结构发布时再做合并压缩也行。收据这个体量的工具直接在单文件里写完最省事维护成本也低。5.3 UTF-8的BOM与Excel打开乱码收据数据复制到Excel或导出CSV时中文变乱码是高频问题。根因不是页面编码错误而是文本文件缺少BOM标识Excel默认按ANSI解码无BOM的UTF-8内容中文自然错乱。解决方法是导出时在CSV字符串最前面加\uFEFF字符。同时确保HTML文档里有声明并且放在head的第一个位置。这两个点做到位Windows下用记事本和Excel打开都不会乱码。本文还有配套的精品资源点击获取
返回列表