HTML CSS JS网页设计实战:打造火车票订票系统完整指南
简介这是一份基于HTML、CSS与JavaScript的火车票订票系统完整项目以综合实战方式将前端页面设计与Java Web后端开发结合起来非常适合前端初学者、Java Web课程设计、实训项目及毕业设计参考。项目完整覆盖车票查询、用户注册登录、表单校验、AJAX异步刷新、车票信息增删改查以及火车站点管理等核心流程能清晰看到页面、服务端与数据库三层之间的协作关系。资源包共405个文件整体仅7.46MB内部分布明确前端部分包含HTML页面、CSS样式表、JavaScript脚本以及png、gif、jpg等界面图片素材后端部分包含Java源码、Servlet处理类、JSP动态页面、class编译文件和jar依赖包另有SQL数据库脚本用于初始化车次、站点、车票和用户账户数据。目前已有461人浏览学习。通过通读源码可以学会请求如何到达Servlet、业务数据如何写入数据库、结果又如何返回到页面同时掌握表单验证与异步更新等常见Web开发技巧适合作为在线票务类项目的直接参考模板。 打开12306查车次时我常想起自己第一次用HTML、CSS、JavaScript做火车票订票系统的那个课程项目。当时想得很简单把查询表单做出来点一下按钮把车次列出来就算完事。可真做起来才发现从一个静态页面到一个能跑通“查车次→选座位→算票价→下订单”完整流程的小系统中间隔着的东西比想象中多得多。这篇文章我会围绕“html css js网页设计”这个核心把一个火车票订票系统从前期的功能拆分、页面布局到交互逻辑实现、数据保存一步步讲清楚并给出可以直接复制的核心代码。适合正在做网页设计大作业的学生、刚学完前端基础想找综合练手项目的新手还有打算把课程设计包装成作品集的初学者。做完这套系统你基本就把前端三大件的核心用法都串了一遍而不是停留在照着教程敲代码、敲完就忘的状态。1. 为什么是车票系统一个练手项目如何覆盖前端三大件1.1 这个项目到底能练到哪些东西火车票订票系统最打动我的一点是它几乎把前端入门阶段的所有知识点都装进了一个真实场景里。HTML部分你需要写表单、按钮、列表、弹窗还要考虑语义化标签怎么用才合理CSS部分flex和grid布局会用到卡片设计、标签、状态提示、响应式适配也都会碰到JavaScript部分更是核心数组的filter、map、sort方法DOM操作事件监听还有localStorage本地存储全都是实际开发里天天用的东西。很多新手学完基础语法之后最大的困惑是“我都会但不知道能做什么”。车票系统恰好给了你一个完整的业务场景让每个知识点都有了用武之地。你写的每一段代码都能在页面上看到实实在在的效果这种正反馈对新手特别重要。1.2 业务复杂度刚刚好选练手项目最怕两种一种太简单比如做个静态个人主页做完除了写几个div和设置颜色啥也没练到另一种太复杂比如一上来就做电商系统涉及用户注册、商品管理、购物车、支付对接新手做到一半就崩溃了。火车票订票系统的复杂度正好卡在中间。它的业务链路清晰——查询车次、浏览余票、选择席位、计算票价、提交订单每一个环节都是现实世界中真实存在的不需要你凭空想象规则。同时数据量可控你只需要维护一个车次数组不需要后端、不需要数据库一个浏览器就能跑完整个闭环。这个项目还有一个优势它比普通课程设计“看起来更完整”。当你把订单保存到localStorage刷新页面后还能看到历史订单时这个项目就已经脱离了“静态网页”的层次可以堂堂正正写进简历作品集里了。2. 动手前的功能清单把“订票系统”拆成看得见摸得着的模块2.1 页面规划单页多视图而不是一堆散乱的HTML文件我建议不要做成多个HTML页面跳转而是做一个单页应用风格的页面通过JavaScript切换不同区域的显示状态。原因很简单多页面跳转意味着每个页面都要重新加载公共的导航和样式而单页多视图只需要控制display属性的切换代码量更少逻辑也更集中。整个页面划分为四个核心区域顶部导航栏展示系统名称提供“车票预订”和“我的订单”两个入口。查询面板出发地、目的地、出发日期、车型筛选以及查询按钮。车次列表区根据查询条件动态渲染车次卡片支持按时间、历时、票价排序。订单弹窗/订单列表预订时弹出选座与乘客信息表单提交后能在“我的订单”里看到历史记录。2.2 数据模型让程序先有“车次”和“订单”很多新手一上来就写HTML写到一半发现数据不知道怎么放又回头改结构。正确做法是先定义数据模型再写界面。车次数据用一个对象数组表示每个车次包含编号、车型、出发到达站、时间、历时和不同席别的价格与余票。下面是我用的结构const trainData [ { id: G101, type: 高铁, from: 北京南, to: 上海虹桥, depTime: 08:00, arrTime: 13:28, duration: 5小时28分, second: { price: 553, left: 120 }, first: { price: 933, left: 30 } }, { id: D311, type: 动车, from: 北京南, to: 南京南, depTime: 09:30, arrTime: 13:52, duration: 4小时22分, second: { price: 443, left: 80 }, first: { price: 708, left: 15 } } ];订单数据在用户提交时生成包含车次编号、日期、席别、票种、单价、乘客姓名、证件号和订单号。先把数据结构想清楚后面所有查找、筛选、计算、渲染都会顺畅很多。这也是实际开发中“面向数据编程”的基本思路。3. 页面骨架HTML先立结构再谈样式与交互3.1 三大核心区域查询、列表、订单我一直坚持一个习惯先写HTML骨架完全不管它长得好不好看等结构确定后再上CSS。因为浏览器渲染页面时是“结构先行”的DOM结构不清晰后面加样式和脚本都很容易乱。顶部导航和查询面板的结构大概是这样的header classtopbar h1火车票订票系统/h1 nav a href# idbookLink车票预订/a a href# idorderLink我的订单/a /nav /header main section classquery-panel div classform-row label出发站/label input typetext idfrom placeholder例如北京南 label到达站/label input typetext idto placeholder例如上海虹桥 /div div classform-row label出发日期/label input typedate iddate label车型/label select idtrainType option valueall全部车型/option option value高铁高铁/option option value动车动车/option option value普快普快/option /select button idsearchBtn查询车次/button /div /section section classtrain-list idtrainList !-- 车次卡片由JavaScript动态渲染 -- /section /main注意看我用了label和input的搭配虽然例子中没写for属性关联实际使用时建议加上但语义上已经比一堆divspan清晰很多。导航用nav包裹页面主体用main这些都是HTML5标准语义化标签课程作业里加上这些细节很加分。3.2 弹窗与订单区域预订流程的入口“我的订单”可以做成一个隐藏的列表区点导航时显示订票弹窗则用div模拟通过CSS的fixed定位和遮罩层实现。div classmodal-overlay idorderModal div classmodal-box h2填写乘车人信息/h2 p idmodalTrainInfo/p div classform-row label席别/label select idseatType option valuesecond二等座/option option valuefirst一等座/option /select /div div classform-row label票种/label select idticketType option valueadult成人票/option option valuechild儿童票/option option valuestudent学生票/option /select /div div classform-row label乘客姓名/label input typetext idpassengerName /div div classform-row label证件号码/label input typetext ididCard /div p idpricePreview/p button idconfirmOrder确认支付/button button idcloseModal取消/button /div /div结构完整比样式花哨更重要。先确保每一个交互元素都有对应的DOM节点和id后面JavaScript才能精确操作。这一步做完整个页面的“骨架”就已经立起来了。4. CSS布局与视觉把车票列表做出真实产品的质感4.1 布局方案flex和grid怎么配合CSS这一层我的经验是不要死记flex还是grid按场景选就行。整体页面用纵向流动导航用flex横向排列查询面板内部用flex换行车次列表用grid网格。核心布局代码如下* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Microsoft YaHei, PingFang SC, sans-serif; background: #f0f4f8; color: #1f2d3d; } .topbar { display: flex; justify-content: space-between; align-items: center; padding: 16px 40px; background: #1a6fb5; color: #fff; } .topbar nav a { color: #fff; text-decoration: none; margin-left: 24px; } .query-panel { max-width: 1100px; margin: 30px auto; padding: 24px; background: #fff; border-radius: 10px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); } .form-row { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-bottom: 12px; } .train-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; max-width: 1100px; margin: 0 auto 40px; }这套组合非常典型外层容器用flex处理“一行放几个控件”的问题车次卡片用grid自动换行排列不管屏幕多宽卡片都能均匀撑满。auto-fill和minmax的组合是让列表自带响应式能力的最简单方式不用写一行媒体查询卡片宽度低于320px就自动换列。4.2 组件细节车票卡片、标签、按钮和弹窗车次卡片是一个信息密度比较高的组件时间、车站、历时、席别价格全都要在有限空间里展示清楚。我习惯用左侧蓝色边框白色卡片的设计主次分明。.train-card { background: #fff; border-left: 4px solid #1a6fb5; border-radius: 8px; padding: 18px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06); transition: transform 0.2s ease, box-shadow 0.2s ease; } .train-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); } .train-card .time-row { display: flex; justify-content: space-between; align-items: center; font-size: 22px; font-weight: 600; } .train-card .price { color: #f56c2d; font-size: 20px; font-weight: 700; } .train-tag { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 12px; color: #1a6fb5; background: #e8f2fa; } .btn-primary { padding: 10px 24px; border: none; border-radius: 6px; background: #1a6fb5; color: #fff; cursor: pointer; font-size: 14px; } .btn-primary:hover { background: #155a92; }这些细节看起来不起眼但对页面质感影响很大。hover时卡片轻微上浮价格用醒目的橙色车型信息做成小圆角标签都是模仿真实票务App的视觉语言。课程作业里只要拥有这些细节老师一眼就能看出不是随便拼凑的。弹窗要注意z-index问题遮罩层和内容层的层级要分开设置.modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 999; justify-content: center; align-items: center; } .modal-overlay.active { display: flex; } .modal-box { background: #fff; padding: 30px; border-radius: 10px; width: 420px; max-width: 90%; z-index: 1000; }遮罩层覆盖全屏并半透明内容层居中显示使用flex active类切换显示状态比直接操作display更优雅也方便后续做过渡动画。4.3 响应式处理手机屏幕上的车票列表火车票订票系统的用户大多在手机上操作所以响应式是必须做的。最简单的方案是在项目写完后用浏览器开发者工具切换到手机模拟器逐个检查布局是否错乱。我常用的两个断点768px以下导航栏换成纵向排列查询面板的表单行改为flex-direction: column让所有控件堆叠起来480px以下车次卡片的字号适当缩小避免时间数字溢出。media (max-width: 768px) { .topbar { flex-direction: column; padding: 12px; } .form-row { flex-direction: column; align-items: stretch; } .train-list { grid-template-columns: 1fr; padding: 0 16px; } }不要一上来就写一堆媒体查询先做完桌面版再用手机模拟器看哪里“碎了”就修哪里效率高得多。5. 查询、筛选与计算JavaScript数据处理的全过程5.1 数据定义与渲染分离JavaScript这部分是这个项目最锻炼人的地方。我强烈建议把“数据”和“渲染”分开思考数据始终存在于trainData数组里页面上的卡片只是数据的一次“快照”。这样筛选、排序、计算票价都只需要操作数据最后统一调用渲染函数即可。核心渲染函数梳理如下先获取当前要显示的车次数组循环生成卡片的HTML字符串一次性赋给容器元素的innerHTML。虽然目前主流框架都倡导用虚拟DOM但在原生DOM操作的学习阶段innerHTML拼字符串仍然是最直观、最好理解的方式。function renderList(list) { const container document.getElementById(trainList); if (list.length 0) { container.innerHTML p classempty-tip没有找到符合条件的车次请调整查询条件。/p; return; } container.innerHTML list.map(train div classtrain-card div classtime-row span${train.depTime}/span span classduration${train.duration}/span span${train.arrTime}/span /div div classstation-row span${train.from}/span span→/span span${train.to}/span /div div classinfo-row span classtrain-tag${train.type}/span span${train.id}/span /div div classbuy-row span classprice二等座 ¥${train.second.price}/span span余${train.second.left}张/span button classbtn-primary>function searchTrains() { const from document.getElementById(from).value.trim(); const to document.getElementById(to).value.trim(); const date document.getElementById(date).value; const type document.getElementById(trainType).value; const result trainData.filter(item { const matchFrom !from || item.from.includes(from); const matchTo !to || item.to.includes(to); const matchDate !date || item.date date; const matchType type all || item.type type; return matchFrom matchTo matchDate matchType; }); currentList result; renderList(result); }这里有个很关键的逻辑当输入框为空时条件应该“不生效”而不是“去匹配空字符串”。!from表示“出发地没填就不限制”item.from.includes(from)表示“填了就从站名包含关系去匹配”。这个写法可以举一反三任何多条件筛选表单都能套用。筛选下拉框的change事件里只需要把车型条件叠加到当前列表上。注意不能直接过滤trainData否则前面已经缩小过的结果会被“重置”回全部数据。所以我在全局维护了一个currentList变量记录最近一次查询的结果。5.3 余票提示与排序让列表具备实用价值真实订票系统里用户对余票数量特别敏感。我在渲染时做了一个简单的状态判断余票大于20张显示充足低于10张显示黄色提醒为0时禁用预订按钮。function getSeatStatus(left) { if (left 20) return span classseat-ok有票/span; if (left 0) return span classseat-warn仅剩${left}张/span; return span classseat-sold已售罄/span; }排序则用sort方法解决。车次出发时间是“HH:mm”格式字符串比较能直接按字典序得到正确结果不需要转时间戳。票价排序需要parseFloat转换后再比较。document.getElementById(sortBy).addEventListener(change, function() { const field this.value; const sorted [...currentList].sort((a, b) { if (field price) { return a.second.price - b.second.price; } return a.depTime.localeCompare(b.depTime); }); renderList(sorted); });注意[...currentList]的浅拷贝这一点很重要直接对原数组sort会改变引用影响后续操作拷贝出来排序就不会污染源数据。6. 下单闭环与localStorage持久化从一个查询页变成“系统”6.1 选座与票价计算规则越贴近现实越好点击“预订”按钮后弹窗显示车次信息用户选择席别、票种、填写乘客信息。此时价格需要实时预览逻辑是先根据席别拿到基础票价再按票种计算折后价。我用了一个很简单的规则儿童票半价学生票七五折保留整数成人票原价。现实中不同席别的折扣规则更复杂但练手项目里算法清晰最重要。function calcPrice(basePrice, ticketType) { if (ticketType child) return Math.round(basePrice * 0.5); if (ticketType student) return Math.round(basePrice * 0.75); return basePrice; } function updatePricePreview() { const trainId document.getElementById(modalTrainInfo).dataset.trainId; const train trainData.find(item item.id trainId); const seatType document.getElementById(seatType).value; const ticketType document.getElementById(ticketType).value; const base train[seatType].price; const total calcPrice(base, ticketType); document.getElementById(pricePreview).textContent 应付金额¥${total}元; }这里train[seatType]用到了通过变量取对象属性的技巧seatType的值正好是second或first和数据结构里的字段名一一对应不需要写if-else来判断席别。这种“数据结构与业务逻辑对齐”的设计思路工作中很常用。6.2 订单提交与本地存储刷新页面数据不丢订单提交后生成一个订单对象推入localStorage里的orders数组。localStorage只能存字符串所以必须JSON.stringify再存读取时JSON.parse再转回来。function saveOrder(order) { const orders JSON.parse(localStorage.getItem(orders) || []); orders.push(order); localStorage.setItem(orders, JSON.stringify(orders)); } document.getElementById(confirmOrder).addEventListener(click, function() { const order { orderId: TC Date.now(), trainId: document.getElementById(modalTrainInfo).dataset.trainId, date: document.getElementById(date).value, seatType: document.getElementById(seatType).value, ticketType: document.getElementById(ticketType).value, passengerName: document.getElementById(passengerName).value.trim(), idCard: document.getElementById(idCard).value.trim(), price: parseFloat(document.getElementById(pricePreview).textContent.replace(应付金额¥, ).replace(元, )) }; saveOrder(order); closeModal(); alert(购票成功订单号 order.orderId); });订单号用TC当前时间戳生成保证不重复。所有字段在提交前都做了一次trim处理这是我前几次测试时被空格坑过之后养成的习惯。6.3 从“能看”到“能用”闭环的价值做完订单的保存功能这个项目的性质就变了。以前刷新页面所有操作全部消失现在信息存进了浏览器关闭页面再打开“我的订单”里依然能看到真实的历史记录。虽然这只是localStorage离真正的后端数据库差着十万八千里但产品的“闭环感”已经有了。我也建议在这个阶段把“我的订单”视图补上从localStorage读取出所有订单用表格展示时间、车次、乘客、金额、状态。这相当于你的系统多了一个独立的“管理页面”虽然代码量不大但完成度提升非常明显。7. 踩坑记录这些细节最容易让新手翻车7.1 数值、日期与字符串的类型陷阱input输入框的value永远是字符串这看起来是常识但测试时最容易出错。价格计算时如果用字符串直接做加法得到的是字符串拼接而不是数值相加。我吃过一次亏选了两张成人票显示应付金额是“553553”而不是1106。解决方案是parseFloat强转计算完再转回字符串展示。日期输入框的问题更隐蔽。HTML的input typedate传出的值格式是“YYYY-MM-DD”这种格式字符串在做比较时是安全的可以直接用。但如果你用new Date(2025-01-01)有些浏览器会按UTC时区解析而本地显示是按东八区就会出现日期差一天的情况。我的建议是练手项目里日期只做字符串比较不要随便转Date对象。7.2 动态渲染元素的事件绑定问题新手最容易踩的坑是车次卡片是渲染出来之后才存在于页面上的直接在“预订”按钮上addEventListener是无效的因为脚本执行的那一刻按钮还不存在。我当时也卡了很久后来学会事件委托才解决。把点击事件委托给车次列表容器通过event.target判断是否点击了预订按钮再用data-id拿到车次编号。document.getElementById(trainList).addEventListener(click, function(e) { const btn e.target.closest(.btn-primary); if (!btn) return; const trainId btn.dataset.id; openOrderModal(trainId); });e.target.closest比直接判断e.target.classList.contains更稳妥因为如果点击的是按钮内部的文字节点或子元素closest可以自动向上找到按钮元素。7.3 弹窗层叠与样式覆盖弹窗出现后滚动页面能看到背后的内容或者遮罩层盖不住底部导航这类问题通常是定位和层级引起的。fixed定位的遮罩层一定要同时设置top、left、right、bottom为0而不是只写width: 100%和height: 100%后者在移动端地址栏收缩时会出问题。还有一个小细节弹窗打开后给body加overflow: hidden禁止背景滚动关闭时再移除。这个交互细节很多真实产品都做了但课程作业里几乎没人会注意做了就是加分项。7.4 样式污染全局选择器要收敛写CSS时为了方便可能直接写section { padding: 20px; }这类全局选择器结果某个板块的间距突然不对了排查半天发现是全局样式在作怪。我的建议是所有样式都通过类名作用类名用语义化命名比如query-panel、train-card、modal-box尽量不要用标签选择器设置布局属性这样后续调整样式时不会牵一发而动全身。火车票订票系统这类项目最大的价值不在于代码量有多少而在于它逼着你把HTML、CSS、JavaScript真正组合起来使用。做完之后我对前端开发的整体认知清晰了很多结构、样式、行为三层各司其职数据是核心驱动页面上看到的一切都只是数据的不同表现形式。如果时间充裕后续还可以继续加功能比如按日期切换不同车次班次、加入座位图选座、增加订单删除操作每加一个功能你对这套代码的理解就会又深一层。本文还有配套的精品资源点击获取