
大学第一次web前端开发作业老师丢来一个非常朴素的需求用HTML和CSS做一张汇款单要求单子看着像真的打印出来能拿去柜台填顺便把填写金额自动转成大写。不少同学觉得这就是个“画表格”的活真正动手才发现一张汇款单几乎能把前端入门最基础也最磨人的东西全串起来——结构化HTML、表格边框合并、表单控件对齐、盒模型、打印样式再加上一点点原生JavaScript。这篇文章就是我当年写完作业之后整理的复盘笔记。无论你是正在应付作业的学生还是刚学前端想找个小项目练手的开发者都可以照着这个思路做我尽量把为什么这么做也讲清楚。1. 一张汇款单到底在考什么很多同学第一次看到“汇款单”三个字脑子里冒出来的就是“用table画格子”。这个判断不算错但只看到了表面。前端开发里的表单页面尤其是这种模拟纸质单证的页面最值钱的不是把格子画出来而是把信息层级、数据采集、视觉还原和打印输出几个维度全部处理好。老师选汇款单当第一次作业恰恰是因为这个题目的边界非常清晰不需要后端、不需要框架、不需要复杂的动效但必须把基本功打扎实。我拿到题目之后没有急着写代码先去找了一张真实的汇款单看。纸质汇款的要素其实很固定票号、汇款种类、金额小写和大写、收款人信息、汇款人信息、附言、日期、经办与复核。把这些字段按业务关系拆成小组页面结构就自然出来了。如果你上来就写一个几米长的表格写着写着就会发现自己不断在改标签、调colspan最后代码和样式全乱。1.1 别把“画表格”想简单了汇款单作业最迷惑人的地方在于它看起来只有一个页面、几个输入框好像工作量很小。可是真正要用HTML还原一张单子你得先想清楚三个问题页面上的表格是用语义化标签老老实实写还是用div拼出来每个输入框和标签之间的对应关系怎么做打印到纸上的时候边框、背景、字体能不能保持和屏幕上一样这三点分别对应着table语义化、label与表单控件关联、CSS打印媒体查询。任何一个没做到位作业看着就“假”。比如很多同学喜欢用divcss去模拟表格在前端开发中这确实是主流布局方式但用在票据单证上会非常别扭div没有表格天然的行列对齐能力你得自己算宽度、清浮动、处理边框重叠打印时分页还会把结构切乱。第一次作业老老实实用table不是技术落后而是选对了场景。除了布局还有信息分组。汇款单上字段很多如果全部堆在一个大表格里阅读体验很差。我当时把单子分成三个区块单证编号区票号、种类、金额区小写、大写、收付款双方信息区姓名、地址、附言。每个区块一个table外面用main包裹再用CSS统一边框风格。这样代码结构清晰日后加校验、加样式都不需要推倒重来。1.2 字段拆解汇款单应该有哪些内容把一张真实汇款单的字段拆开你会得到一个很有意思的清单。单证头部包括汇票号码、汇款种类金额区是汇款金额、手续费、大写金额中间是收款人姓名、收款人详细地址、汇款人姓名、汇款人详细地址底部是附言、经办人、复核人、日期。这些字段虽然多但性质很相似大部分是单个文本框地址是长文本框附言是多行文本区种类是下拉选择大写金额是动态生成的文字。我们完全可以用统一的一套样式处理输入框再为特殊字段留出扩展空间。我在代码里给每个input都加了name属性比如billNo、payeeName、remitterName。这不是多此一举。name属性不仅让表单提交有了字段名也是后面写JavaScript获取元素时最稳定的接口。用querySelector配合name选择器比用一堆id清爽得多。另外每个字段最好都和label的for属性绑定点击标签文字就能聚焦到对应输入框。这一点看似不起眼却是很多初学前端的人完全忽略的可访问性细节。2. 页面结构用HTML搭出汇款单骨架结构是整个页面的地基。我习惯先把HTML骨架完整写出来再动CSS。这样后面所有样式都有明确的“挂靠点”不会出现为了调一个间距去到处改class的情况。2.1 先搭结构别急着美化我把汇款单包裹在main.remit-slip里内部直接用form包住所有输入控件。这样既方便提交校验也告诉浏览器“这个页面是一个可交互的表单”语义上比散落的div更准确。下面是我当时写的核心HTML骨架你可以直接参考!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title汇款单/title link relstylesheet hrefstyle.css /head body main classremit-slip form idremitForm autocompleteoff header classslip-head h1汇款单/h1 table tr th汇票号码/th tdinput typetext namebillNo placeholder必填/td th汇款种类/th td select nametype option valuenormal普通汇款/option option valueurgent加急汇款/option option valuepassword密码汇款/option /select /td /tr /table /header section classamount-area table tr th汇款金额/th tdinput typetext nameamount idamount inputmodedecimal placeholder0.00/td th大写金额/th td classamount-upper idamountUpper人民币/td /tr /table /section section classparty-area table tr th收款人姓名/th tdinput typetext namepayeeName/td th汇款人姓名/th tdinput typetext nameremitterName/td /tr tr th收款人地址/th td colspan3input typetext namepayeeAddress/td /tr tr th汇款人地址/th td colspan3input typetext nameremitterAddress/td /tr /table /section section classextra-area table tr th附言/th td colspan3textarea namemessage rows2/textarea/td /tr tr th经办人/th tdinput typetext nameoperator/td th复核人/th tdinput typetext namereviewer/td /tr /table /section div classtoolbar no-print button typesubmit classbtn-primary校验并提交/button button typereset classbtn-reset重置/button /div /form /main script srcscript.js/script /body /html这个结构有几个细节值得说。地址栏我用了colspan3让地址输入框横跨右侧三个单元格视觉上更接近纸质单据的长横线。textarea放在表格里也没问题只要给它设置好CSS宽度和resize:none它在表格里的表现就会非常稳定。后面的toolbar按钮区加上no-print类是为了在打印时隐藏操作按钮这个后面会单独讲。2.2 表格布局 vs 绝对定位 vs 网格布局写汇款单之前我一度纠结要不要直接用div精确画位置。后来试下来发现绝对定位看似自由实际是个坑你要用固定的px值去卡每个输入框的位置一旦某个字段内容过长或者浏览器缩放页面就会错位。现代CSS里的Grid和Flexbox虽然也能做表单但它们更适合做流式布局和整体页面排版对于“每一行有几个固定列、表头宽度一致”的票据场景table反而有天然优势——表格的列宽、单元格合并、垂直对齐都是开箱即用的。表格布局还有一个好处是打印分页时更稳定。表格的行在打印时不容易被截断而div绝对定位的页面打印出来经常会出现内容重叠。如果你是第一次做这类作业我的建议非常直接结构用table列宽用table-layout:fixed统一控制外部再套一个main用来设置单据的整体尺寸和阴影。不要觉得用table就不够“现代”选工具的核心标准是合适而不是看起来高级。2.3 label不是摆设把可访问性做对很多同学的HTML里只有input没有label。这样页面上也能显示输入框单击输入框也能聚焦但辅助工具没法知道这个输入框是干什么的。汇款单这种密集表单可访问性尤其重要。正确的做法是让label通过for属性指向input的id而不是把文字和输入框随便放在同一个单元格里。我这里为了让点击文字也能聚焦可以在每个th里不放label而是用CSS样式给th一个“伪标签”的效果但更正规的写法是在单元格里写label。比如“收款人姓名”这一栏如果你想做得语义化更完整可以这样写tr thlabel forpayeeName收款人姓名/label/th tdinput typetext namepayeeName idpayeeName/td /tr这样屏幕阅读器会把label和input关联起来鼠标点击“收款人姓名”四个字时输入框也会自动获得焦点。对第一次作业来说这算一个非常加分的细节。3. CSS样式从“有框”到“像一张单子”HTML搭好之后页面只是“有一堆框”。真正的难点在CSS如何让这些框看起来不是网页默认的粗陋表格而是一张带着纸质的正式汇款单。这里我按照盒模型、边框、字体、表单控件四个维度依次处理。3.1 盒模型与全局重置第一次做页面时我吃过一个亏input在表格里明明设置了width:100%结果还是撑破单元格。原因就是默认的content-box盒模型width只算内容区域不算padding和border。只要给input加了内边距和边框它的实际宽度就会超过父容器。解决方法是全局设置* { box-sizing: border-box; margin: 0; padding: 0; }这个声明一定要放在所有CSS的最前面并且优先级不能被覆盖。border-box让width包含了padding和border输入框再怎么加样式也不会把表格撑破。很多前端培训里说“盒子模型是基础”但真正体会到这个基础的作用往往是靠这种撑破单元格的报错。除此之外我还对h1、table、input、select、textarea做了统一reset避免不同浏览器里默认间距和字体不一致。3.2 用表格边框画出票据的“框感”纸质汇款单最典型的特点是粗边框、细内线、表头底色清浅。我用了一个非常简单的方案外层表格使用border-collapse:collapse合并相邻边框让单元格之间的线是单线而不是双线表格外框用较粗的实线内部单元格用1px的浅灰色线。这样视觉上既有票据的框感又不会像默认table那样生硬。.remit-slip { width: 700px; margin: 40px auto; padding: 24px; background: #fdf8ef; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); font-family: FangSong, 仿宋, STFangsong, SimSun, serif; } .remit-slip table { width: 100%; border-collapse: collapse; table-layout: fixed; margin-bottom: 16px; } .remit-slip th, .remit-slip td { border: 1px solid #8b7d6b; padding: 8px 10px; height: 44px; vertical-align: middle; } .remit-slip th { width: 110px; background: #f5f1e8; text-align: center; letter-spacing: 2px; font-weight: normal; }我给th设置了固定宽度110px配合table-layout:fixed后面的td会自动平分剩余宽度。这样一来地址这种需要跨多格的输入框就能自然拉长而不用去精确计算每一列的像素值。实际做完之后屏幕上看起来和打印出来都非常接近纸质单证。3.3 字体、间距和“纸质感”纸质单证其实没有太多花哨设计它的气质主要来自字体和间距。中文票据常用的字体是仿宋或者宋体西文和数字用Times New Roman这类衬线体。我直接设置了一条字体栈FangSong优先然后是系统的仿宋最后兜底到serif。这样在Windows和macOS上都能找到合适的字体不会出现字体缺失后变成黑体的情况。单据整体宽度我控制在700px左右居中显示。每个区块之间的margin-bottom为16px让抬头、金额区、双方信息区看起来是一张单子上的几个分区而不是挤成一团。表格的内部间距我用了8px上下、10px左右高度44px。这个高度对普通输入框来说不会太紧也不会太松打印出来后手写填字也有足够的空间。字号的细节更讲究th里的文字要大一点、加一点字间距td里的input不需要太大不然会把单元格顶得变形。3.4 输入框样式统一默认的input在浏览器里长得很“程序员”尤其是边框、背景色、聚焦状态各不相同。为了让汇款单像真正的纸质票据我把所有输入控件统一成“无边框盒子底部细线”样式input, select, textarea { box-sizing: border-box; width: 100%; border: none; border-bottom: 1px solid #b0a08a; background: transparent; font-size: 16px; padding: 4px 2px; border-radius: 0; } input:focus, select:focus, textarea:focus { outline: none; border-bottom-color: #b33a2a; } textarea { resize: none; }这个做法的好处是单子上的填写区域不是用方框圈起来的而是像真正汇款单一样用下划线提示填写位置视觉上更轻盈。聚焦时下划线变成深红色用户马上知道当前编辑的是哪个字段。这里有个前端开发的老经验移动端的input字体大小如果小于16pxiOS会自动在聚焦时放大页面。所以我把字号统一设成16px既适合桌面端也避免手机上突然缩放。4. 除了静态页面我顺手加了点原生JS汇款单如果只做静态展示交作业也够了。但既然页面里已经有金额字段和大写金额栏我就想顺手做个联动用户填阿拉伯数字大写金额自动更新。顺带把必填项校验一起做上。这一部分不用jQuery、不用框架纯原生JavaScript足够完成。4.1 金额大写转换从需求到手写实现金额大写转换看起来是数学题做起来其实是字符串处理题。核心规则不难把数字0到9映射成“零壹贰叁肆伍陆柒捌玖”整数部分按四位一组处理从右往左分别是个十百千每四个一组再加一个段位单位“万、亿”小数部分看角和分。难点在于零的规则连续出现多个零只能写一个零零在整组的末尾不写整数部分为零、小数部分有值时需要在“元”后面处理“零角零分”的读法。我这里给一个课堂演示级的简化函数只处理0到9999.99元方便你看懂思路function amountToChinese(value) { const nums [零, 壹, 贰, 叁, 肆, 伍, 陆, 柒, 捌, 玖]; const units [仟, 佰, 拾, ]; const num Math.round(Number(value) * 100) / 100; if (isNaN(num) || num 0 || num 9999.99) { return 金额超出演示范围; } const [intPart, decPart] num.toFixed(2).split(.); const intStr intPart.padStart(4, 0); let intResult ; let pendingZero false; for (let i 0; i 4; i) { const d Number(intStr[i]); if (d 0) { pendingZero true; } else { if (pendingZero intResult) intResult 零; pendingZero false; intResult nums[d] units[i]; } } intResult intResult.replace(/零$/, ) || 零; let result ; if (intResult ! 零) result intResult 元; else result 零元; const jiao Number(decPart[0]); const fen Number(decPart[1]); if (jiao 0 fen 0) { result 整; } else { if (jiao 0) { result nums[jiao] 角; } else if (intResult ! 零) { result 零; } if (fen 0) result nums[fen] 分; } return result; }这个函数故意没有处理万亿级因为作为第一次作业演示“输入金额联动大写”已经足够。如果你想扩展到位数更大规律是一样的把整数部分从右往左每四位切成一组每组重复“千百十”的转换组与组之间再插回“万、亿”这些段位词。真正要做生产级的金额转换还要花大量时间处理零的边界这是一个非常好的练手题目。4.2 输入联动和表单校验有了转换函数剩下的联动就很简单。我给金额输入框绑定input事件输入后立刻更新大写文本同时过滤掉非数字和小数点字符避免用户敲个字母进去。代码const amountInput document.getElementById(amount); const amountUpper document.getElementById(amountUpper); amountInput.addEventListener(input, function () { const cleaned this.value.replace(/[^\d.]/g, ); this.value cleaned; amountUpper.textContent 人民币 amountToChinese(cleaned); });校验部分我用form的submit事件统一处理。必填字段取出来依次检查如果为空就弹窗提示并阻止提交。注意这里要用e.preventDefault()而不是只弹个alert就完事否则在刷新页面时能看到浏览器自己跳过的原生校验效果后续作业会分不清是代码生效还是浏览器兜底。const remitForm document.getElementById(remitForm); remitForm.addEventListener(submit, function (e) { const requiredFields [billNo, payeeName, remitterName, payeeAddress]; for (const name of requiredFields) { const field remitForm.querySelector([name${name}]); if (!field.value.trim()) { alert(请填写必填字段); e.preventDefault(); return; } } });这段代码把表单校验做成了“防呆”操作用户差一格不填提交按钮就会提醒。我第一次做的时候漏写了e.preventDefault结果点击提交后页面直接刷新校验完全失效。排查了半天才意识到事件处理函数里不阻止默认行为表单照样会按自己的方式提交。对没有后端的静态页面来说这个细节格外重要。5. 打印友好让汇款单真的能“拿出去”如果只是交作业屏幕显示好可能就够了。但汇款单的使用场景天然是打印出来填写所以打印样式是我整个作业里最重视的部分。没有打印样式的话一按CtrlP屏幕上的淡黄色背景、阴影、按钮全会变成纸上乱七八糟的色块。5.1 用media print隐藏按钮、还原底色打印样式的核心就三件事去掉屏幕装饰、保证边框清晰、隐藏不需要打印的元素。我写了这样一段media print { body { background: #fff; } .remit-slip { width: auto; margin: 0; padding: 0; box-shadow: none; background: #fff; } .toolbar, .no-print { display: none !important; } input, select, textarea { border-bottom: 1px solid #000; font-size: 14px; } }把背景色去掉是因为大部分家用打印机不会打印背景颜色就算设置了也经常被浏览器默认跳过。把阴影去掉是避免纸上出现奇怪的灰色边缘。操作按钮隐藏以后打印预览里就只有一张干干净净的汇款单。5.2 给打印页面设置物理尺寸纸张尺寸也需要声明一下。这里用page规则把打印区域设置成接近真实汇款单的窄长比例。比如你想让单子适应一张A5大小的纸可以这样写page { size: 210mm 140mm; margin: 5mm; }需要提醒一句page的size属性在不同浏览器里的支持程度不一样。Chrome对size的支持一直不算好很多情况下你还是要靠用户在打印对话框里手动选纸张方向。所以我的做法是表格内部所有列宽都设置好相对宽度这样即使纸张大小不同页面内容也能缩放排版而不是固定像素值直接超出页面。6. 踩过的坑、排查思路和提交前清单写汇款单这个作业虽然只有几百行代码但我踩的坑一点不比做复杂项目少。很多问题看起来微不足道却能卡你一晚上。这一部分我整理成了一份“速查表”希望能帮你少走弯路。6.1 我第一次踩过的五个坑第一个坑是input撑破单元格。原因就是盒模型前面已经说过解决方案统一加box-sizing:border-box。第二个坑是select在macOS和Windows上的样式完全不同。最简单的解决办法是给select设置一个固定的height和border并且不要用背景图片美化不然很容易在某一个浏览器里出现文字被截断。第三个坑是input的自动填充。Chrome会自动把历史输入项填成黄底色很难看。一个临时方案是给表单加autocompleteoff但这不是万能的正式项目有更复杂的处理方式。第四个坑是金额大写函数里的中文“零”。比如输入1001元函数输出“壹仟零零壹元”这一眼就能看出函数有问题。后来我用“pendingZero”标记连续零只补一个再配合正则去掉结尾的零才算把输出弄干净。这个教训说明处理中文数字不要只想“一个一个拼”必须先把“连续零”和“末尾零”这两个状态理解透。第五个坑是打印时input的边框消失了。屏幕上看是下划线但打印预览里却变成一片空白。原因是input的背景色是transparent浏览器打印时默认不打印背景和边框。解决办法就是在media print里给input、select、textarea重新设置黑色下边框。我把这些问题整理成了表格现象原因解决input撑破表格默认content-box盒模型设置* { box-sizing: border-box; }输入框聚焦时页面放大移动端字体小于16px统一设置font-size: 16px打印时输入框线消失浏览器不打印背景和浅色边框media print中加黑色边框大写金额出现多个“零”连续零处理逻辑缺失使用pendingZero标记Chrome自动填充黄底浏览器默认样式临时用autocompleteoff6.2 提交前检查清单作业写完并不是结束我习惯在提交前按清单过一遍。DOCTYPE声明必须是html5charset必须utf-8否则中文可能出现乱码所有缩进要统一标签要闭合id和name不要重复节点命名最好见名知意CSS里不要大量使用px硬调位置优先用margin和padding配合表格布局。还要在至少两个浏览器里打开页面检查一遍Chrome、Firefox、Edge里对input和select的默认样式差别不小。如果想再拿高一点分可以打开浏览器开发者工具切到移动端模拟视图再把窗口宽度拖到375px看一下。虽然汇款单是打印场景但老师如果在手机上打开你的作业一堆输入框挤在一起也会扣印象分。一个简单的做法是给.remit-slip加max-width:100%并设置overflow-x:auto保证小屏幕下横向滚动而不是页面整体变形。这份清单看起来简单但每次都能拦下几个潜在的致命错误。我第一次提交作业之前就是因为忘了检查HTML标签闭合导致表格里多了一个孤儿tr整个页面布局全乱。后来我养成了一个习惯写完代码先格式化再手动数一遍表格里的tr和td数量。老话说得好页面越是“简单”越考验细心程度。