拓冰建站拓冰建站
首页 / 资讯中心 / 正文

纯前端三件套实现火车票订票系统:从页面设计到数据流转全解析

简介这是一套基于HTML、CSS与JavaScript实现的火车票订票系统网页设计完整源码并整合Servlet/JSP与MySQL数据库面向Web前端学习者、Java Web初学者以及需要课程设计或毕业设计参考的高校学生。压缩包共405个文件约7.46MB含47个CSS、30个JS、17个JSP、25个Java源文件与25个class文件另有多张PNG/GIF/JPG图片素材涵盖页面样式、动态交互、后端逻辑与SQL脚本项目目录清晰完整。目前已有461人学习下载。资源可直接部署运行代码注释清晰能够帮助读者理解用户登录注册、车票查询、订单处理、后台数据增删改查等核心流程掌握Ajax异步交互、Servlet请求处理、JSP动态页面生成与数据库操作的完整Web开发链路是提升前后端综合技能的实用案例。1. 项目概述与需求拆解1.1 这个火车票订票系统到底是什么如果你在搜索引擎里点进这篇文章大概率是因为你正在为“网页设计课设”“前端大作业”或者“个人作品集项目”发愁手上只有一个题目html css js网页设计——火车票订票系统。先说结论这个项目用纯前端三件套就能做得像模像样不需要后端不需要数据库甚至不需要装任何框架把html、css、js三个文件写好浏览器一打开就能跑。所谓火车票订票系统拆开来说就是用户能完成车次查询、选择车次、填写乘客信息、提交订单、看到车票信息这一整条流程的网页应用。和真实的12306比起来我们做的这个是一个模拟系统数据自己定义逻辑自己写但业务流程的完整度可以做到很高。很多初学者容易把这类项目做成“只有一个查询框”的静态页面那评分和效果都会打折扣。真正拿得出手的版本要能看到车次列表的动态渲染、余票数字的变化、订单数据的本地存储以及各个页面之间通过js进行的数据流转。我当年做类似项目的时候也走过弯路一开始只顾着把页面画好看结果js逻辑一塌糊涂点击按钮没反应票数不减刷新页面数据全丢。后来重新梳理了一遍需求和交互流程才把这个项目做扎实。这篇文章我会把整个系统的设计思路、功能模块、核心代码实现、以及我踩过的坑全部拆开讲适合已经会html/css/js基础语法、但没做过完整项目的同学直接照着复现。1.2 技术选型为什么坚持纯前端三件套做火车票订票系统技术方案其实有好几条路可以用Vue或React配合组件库快速搭界面也可以上Node.js写接口配合数据库做前后端分离更可以只用原生html/css/js把所有事情写在本地。为什么要选第三条路理由很现实这门课教的就是html、css、js评分标准看的就是你对这三样基本功的掌握程度贸然上框架反而可能因为“过度设计”被扣分。纯前端的核心优势在于部署简单、逻辑透明。不需要配置服务器环境不需要考虑跨域问题双击index.html文件就能看到效果。数据层面我们用js中的数组和对象模拟数据库把车次信息、订单记录都维护在内存里再更进一步用localStorage把订单数据持久化保存刷新页面之后数据还在这个细节就能让你的项目在同类作品里拉开差距。有人会担心不用框架代码会不会很乱这个担心是正常的。但反过来想框架本身就是对原生能力的封装如果你能把原生js的模块化写法、事件委托、数据驱动视图这些思路吃透后面学任何框架都会轻松很多。火车票订票系统的业务逻辑不算复杂车次数据就是一组对象数组购票操作就是数组的增删改查用原生js写起来完全hold住而且你能清楚地看到每一行代码做了什么这对答辩时讲清思路特别有帮助。1.3 完整功能模块预览按照真实订票系统的用户旅程来拆这个项目的功能模块可以划分为四个部分车次查询模块出发地、目的地、出发日期的筛选支持模糊匹配和精确匹配两种方式查询结果以卡片或表格形式动态渲染。车次列表展示模块展示车次号、出发到达时间、历时、座位等级、余票数量、票价信息其中余票数量需要实时变化。订单提交模块选中车次后进入订单确认页填写乘车人姓名、身份证号、联系电话生成订单号和座位信息。订单管理模块查看已提交的订单列表支持取消订单释放余票、清空全部记录。这四个模块对应四个核心页面场景首页查询入口、列表页车次展示、订单确认页信息填写提交、订单管理页历史记录。如果想把项目做得更有完成度还可以加入登录弹窗、城市数据联动、日期选择限制等细节让整个系统看起来更像一个真正可用的产品。2. 整体设计与数据流思路2.1 页面结构规划一页搞定还是多页跳转在动手写第一行代码之前最先要确定的是页面架构。常见的做法有两种多html页面跳转和三页面切换。多页面方案就是让index.html、list.html、order.html各干各的通过a标签或window.location.href跳转单页面方案则是在一个html文件中放多个“页面区块”用js控制显示和隐藏。我强烈推荐第二种方案——单页面多视图模式。原因有三点第一原生js跨页面传数据非常别扭你得用localStorage或url参数中转而单页面模式下js变量可以直接共享车次数据、选中的车次信息、用户填写的内容全都在内存里流转不用做序列化反序列化第二页面切换不需要加载时间界面响应更快用户体验更流畅第三对于课程设计来说一个html文件能把所有功能都展示出来评委打开就能看全不用来回切换文件。具体的结构是这样html里放四个div容器分别对应首页home-page、车次列表list-page、订单确认order-page、历史订单orders-page。初始状态下显示home-page其余div设置display:none隐藏。js里写一个showPage(pageId)函数切换时把目标div的display设为block其他设为none。这个模式其实就是前端框架SPA的路由雏形虽然是土办法但分工明确后续维护也方便。2.2 数据设计怎么用数组和对象模拟数据库火车票系统的核心数据有两类车次信息和订单信息。在js里车次信息可以用一个对象数组来定义每个对象代表一趟车。以我从北京到上海的车次为例let trainData [ { id: 1, trainNumber: G101, from: 北京南, to: 上海虹桥, departTime: 07:00, arriveTime: 11:29, duration: 4小时29分, seatClass: 二等座, price: 553, stock: 120, date: 2025-06-01 }, { id: 2, trainNumber: G103, from: 北京南, to: 上海虹桥, departTime: 08:00, arriveTime: 12:36, duration: 4小时36分, seatClass: 二等座, price: 598, stock: 45, date: 2025-06-01 } ];字段说明id是唯一标识trainNumber是车次号from和to是起终点站departTime和arriveTime用的是字符串形式的24小时制时间duration是运行时长seatClass是席别price是票价单位用number类型方便计算stock是实时余票数date是对应的发车日期。订单数据可以设计成另一个数组let orders [];每提交一条订单就push一个新的对象进去对象里包含订单号、车次号、出发到达信息、乘车人姓名、身份证号、座位号、票价、下单时间等字段。之所以把orders也设计成数组是因为后面做本地存储、遍历展示、取消删除都会非常方便。2.3 为什么“数据驱动视图”是这个项目的核心思维学js最难跨越的一道坎是理解“操作数据”和“操作DOM”的关系。初学者最容易写出的代码是这样的点击“查询”按钮直接去创建一堆DOM节点塞进页面里用户输入了一个数字就直接去改页面上某个span的innerText。这种写法在功能少的时候没问题但一旦逻辑变复杂代码就开始失控你找不到哪个变量对应页面上哪个元素调试起来像大海捞针。火车票订票系统是练习“数据驱动视图”的绝佳场景。核心思想是页面上显示什么完全由js代码里的数据决定的DOM只是一个“展示层”。流程永远是——用户触发事件事件处理函数修改数据数据变化后再调用渲染函数重新生成视图。比如用户点击“订票”按钮我们不是直接去隐藏按钮、弹出表单而是先找到一个trainData数组里对应的车次对象修改它的stock值、存入orders数组然后才去调用renderOrderList()函数把最新的订单列表重新画一遍。这个思路的价值在于你的代码逻辑是单向的、清晰的任何时候想知道某个票还有多少张直接看trainData里对象的stock值就行不用去翻页面上的DOM。后续如果想把项目迁移到Vue或React你会发现Vue的响应式和React的setState本质上干的就是这件事。3. 核心功能与页面实现3.1 查询模块城市选择、日期限制与模糊匹配查询模块是用户进入系统后的第一道门做得好不好直接影响第一印象。页面上需要三个核心字段出发地、目的地、出发日期。出发地和目的地可以使用input输入框加datalist原生素材。datalist是html5提供的“带提示的输入框”既允许用户直接输入文字也可以从下拉列表中选择预设的城市名这个交互非常实用比单纯的select选择框更灵活。示例代码如下input typetext listcity-list idfrom-city placeholder请输入出发城市 datalist idcity-list option value北京/option option value上海/option option value广州/option /datalist出发日期用typedate的input同时通过min属性限制不能选择今天之前的日期。这个min属性在后面的js里会动态设置保证每次打开系统最早可选日期都是当天。查询按钮的click事件触发searchTrains()函数函数内部用filter方法对trainData做筛选。这里有一个很多教程不会讲的细节同时支持精确匹配和模糊匹配。用户输入“北京”能查到输入“北京南”也能查到甚至输入“京”也能查到这就要用到字符串的includes方法。我自己的实现是function searchTrains() { let fromVal document.getElementById(from-city).value.trim(); let toVal document.getElementById(to-city).value.trim(); let dateVal document.getElementById(depart-date).value; let results trainData.filter(item { return item.from.includes(fromVal) item.to.includes(toVal) item.date dateVal; }); renderTrainList(results); }注意这里必须使用比较日期不能用因为date input返回的值就是yyyy-mm-dd格式的字符串和trainData里的date字段格式要严格一致。常见的一个坑的是trainData里date写成了“2025/06/01”或者“2025-6-1”格式一不一致就查不出来这是很多新手排查半天找不到原因的问题源头。3.2 车次列表渲染表格布局与余票状态的动态绑定查询出结果之后要把车次列表展示在页面上。我建议使用表格布局信息密度高、对齐清晰也给后面的订单操作留出操作列。为了让表格风格现代化可以在css里对table做一点美化比如斑马纹、悬浮高亮、圆角边框。这里有一个重要的代码组织思路渲染函数要使用模板字符串来拼接HTML而不是document.createElement逐节点创建。模板字符串可读性高写起来也快但要注意XSS问题。车次数据是我们自己在js里写死的不是用户输入的内容所以用innerHTML是安全的但如果用户输入的乘客姓名要回显到页面上就需要做一下转义处理。一个车次的行应该长这样G101 北京南 - 上海虹桥 07:00 - 11:29 4小时29分 二等座 553元 余票120 [订票]如果余票数量为0那个订票按钮就要置灰禁用同时在余额位置显示“无票”并加红色样式。这种状态绑定用js做非常简单——渲染的时候判断一下stock 0然后动态拼接disabled属性即可。3.3 订票流程从选中车次到提交订单的数据流转用户点击某一行车次的“订票”按钮时我们不需要跳转新页面而是把这个车次对象存到一个全局变量里然后切换到订单确认页面let selectedTrain null; function goToOrderPage(trainId) { selectedTrain trainData.find(item item.id trainId); if (!selectedTrain) return; document.getElementById(order-train-info).innerText selectedTrain.trainNumber selectedTrain.from - selectedTrain.to; document.getElementById(order-price).innerText selectedTrain.price 元; showPage(order-page); }这里的关键在于这也是一段非常重要的思维我们保存的是整个对象而不是保存车次号然后到下一个页面再查一遍。这种思路就是“数据跟着流程走而不是页面跟着流程走”它省去了跨页面传参的麻烦。订单确认页有两块内容上半部分是把选中的车次信息以只读方式展示出来让用户核对这些信息就是从selectedTrain这个对象里读出来的下半部分是一个表单让用户填写乘车人姓名、身份证号和手机号。表单需要做前端校验姓名不能为空、身份证需要是18位数字加X或x、手机号要是11位数字。校验失败给出友好的提示信息比如在输入框下方显示红色小字而不是用alert弹窗。提交订单时要完成这几件事校验表单数据、生成唯一订单号、生成随机座位号、扣减余票库存、把订单对象push进orders数组、调用渲染函数更新页面、跳转到订单完成页面。生成订单号最简单的方式是使用时间戳加随机数的组合比如function generateOrderId() { return D Date.now() Math.floor(Math.random() * 1000); }3.4 订单管理把数据写进localStorage里订单提交完成后如果刷新页面订单全没了体验会非常差而且看起来项目不够完整。所以我们要把orders数组持久化到浏览器的localStorage中。localStorage是浏览器提供的本地存储能力以键值对的形式保存字符串。存入时需要把数组转成字符串读取时再解析回数组function saveOrders() { localStorage.setItem(train_orders, JSON.stringify(orders)); } function loadOrders() { let saved localStorage.getItem(train_orders); orders saved ? JSON.parse(saved) : []; }一个值得注意的细节是localStorage里的数据和js内存里的数据是两份拷贝修改数据时不能只改内存必须每次变更后都要调用saveOrders()把最新状态同步到localStorage反过来页面刷新后要第一时间调用loadOrders()恢复数据。这个双向同步不难但容易漏建议再封装一个updateOrders()函数内部统一处理push和save逻辑function addOrder(order) { orders.push(order); localStorage.setItem(train_orders, JSON.stringify(orders)); }同理取消订单的操作也要同步处理移除orders数组中对应对象、恢复车次的余票找到车次并stock、调用saveOrders()持久化。这一套下来数据的一致性就有保证了。4. 视觉设计与交互提升4.1 主色调与布局方式怎么让界面一眼有质感很多人的网页作业一眼就能看出“学生感”问题通常出在配色和间距上。花一点时间打磨css收益是最直接的。火车票订票系统的视觉基调我建议走“交通蓝浅灰底白色卡片”的路线。蓝色代表科技和可靠适合出行类产品。不要用高饱和度的亮蓝而是选择深蓝或者带有质感的靛蓝作为主色配以浅灰#f5f7fa作为页面背景内容卡片一律白底、圆角16px、带微弱的box-shadow。这样的组合页面清爽看起来专业。布局方面每个页面区块的宽度设定在最大1000px左右居中排列。查询表单放置在页面的视觉中心用一张通栏渐变背景作为Hero区域撑起视觉重量车次列表、订单信息、历史订单则收纳在白色卡片中。导航栏固定在最顶部包含系统Logo和“查询车票/我的订单”两个菜单项用flex实现两侧分布。4.2 细节交互hover反馈、按钮状态和空数据提示如果说配色决定第一印象那交互细节决定用户体验的完成度。至少有三个地方值得用心处理车次列表的每一行要在hover时背景色轻微变化并带有过渡动画让用户清晰地感知到“这一行可以点击”订票按钮在鼠标悬停时要有一个位移或颜色的反馈点击后有按下效果。实现方法是transition加transform的配合不需要复杂的js动画库。按钮的禁用状态要视觉上能感知。余票为0时按钮变灰并变为禁用样式鼠标样式改为not-allowed。这里样式上要注意除了修改背景色还要改cursor和opacity避免禁用样式和普通样式区分度不够。空数据提示。查询不出车次、历史订单为空时页面不能一片空白要显示一个居中的提示信息比如“没有符合条件的车次请调整筛选条件”。这个虽然简单但很多人会忘实际效果却很重要——它让系统显得“完整”而不是“坏了”。4.3 加分技巧城市字段的联想提示与表单动效在完成了基础功能之后有一些小而亮眼的技巧可以显著提升项目高级感。城市输入框里的datalist只能做精确前缀匹配体验还不够好。如果想更进一步可以给出发地和目的地两个输入框加input事件监听在事件里对内置的城市数组做includes模糊匹配再用绝对定位的div动态渲染匹配结果列表点击列表项时把值填入input。这个效果类似于电商网站的搜索联想代码量大概二三十行但视觉冲击力很强答辩时非常加分。表单动效方面用户点击“提交订单”后可以用一个简单的css动画让按钮出现loading状态比如按钮文字变成“提交中...”同时加上一个旋转的圆环动画。由于我们本身是纯前端模拟没有真实的网络请求这个loading动画可以设置为500毫秒后自动跳转到订单成功页面。这种细节能让评委觉得你确实考虑到了真实业务中的交互体验。5. 常见问题与踩坑实录5.1 点击查询没反应控制台报错“Cannot read property of undefined”这个报错是大作业里出现频率最高的bug。通常原因是js脚本的位置放在head标签里而代码中用getElementById查找的DOM元素还没有被浏览器解析生成所以返回了null再对null调用属性就报错了。解决方式有两种把script标签放到body的最底部确保DOM加载完毕后再执行js或者把js代码用window.onload或DOMContentLoaded事件包起来。我推荐前者最简单也最不容易出错。5.2 车次筛选结果不对明明有车却查不出来排列这几个排查方向首先看trainData里的日期字段格式和date input返回的字符串格式是否完全一致注意连字符的横线是短横线还是中划线其次看from字段存的是“北京”还是“北京南”如果用户输入的是“北京”但数据里是“北京南”includes方法依然能匹配到但反过来数据是“北京”用户输入“北京南”就匹配不上最后别忘了在判断条件里加上trim()去掉用户输入的前后空格这个空格问题困扰了我很久因为用户复制城市名粘贴时经常带一个看不见的空格。5.3 提交订单后余票没减少或者刷新后又变回来了余票没减少的原因是订票逻辑里忘了同步修改trainData中对应车次的stock值。记住一个原则页面上看到的余票数字只是渲染结果真正决定数字的是数组里的stock。所有修改必须改到数据源然后再调用渲染函数刷新页面。至于刷新后余票又变回来那是因为你没有对trainData做持久化只对orders做了。如果希望刷新后余票也保持一致需要把trainData也存入localStorage并在每次扣减余票后同步。不过通常课程设计不做这一步也说得过去因为车次数据本来就写死在一开始的代码里就当它是“每天从服务器拉取的初始数据”就好。5.4 页面能从左边划出去直接看到旁边的其他隐藏区块这是display:none和visibility:hidden混淆导致的。在某些容错场景下我见过有同学用visibility控制页面切换但visibility隐藏的元素仍然占据布局空间会导致横向滚动条。单页面切换一定要用display:none和display:block的组合彻底移除布局占位。另外body上可以加overflow-x: hidden兜底防止意外横向溢出。5.5 localStorage的JSON.parse报错订单列表打不开这个坑看起来诡异其实是之前存入了非法JSON字符串。常见原因是调试时直接通过浏览器控制台执行localStorage.setItem存了和正常数据格式不兼容的内容或者存数据时忘了用JSON.stringify直接存了[object Object]这样的字符串。解决方法是在loadOrders函数里做try-catch包裹解析逻辑解析失败就返回空数组不要让它影响整个页面的加载function loadOrders() { try { let saved localStorage.getItem(train_orders); return saved ? JSON.parse(saved) : []; } catch (e) { return []; } }这种健壮性处理也是一个专业开发者必备的意识。项目做到这个程度就不是简单的大作业了而是带一点工程思维的完整前端应用。做完了这个火车票订票系统你会发现自己在好几个方面都有明显进步不再害怕数组对象的各种操作理解了数据驱动视图的思考方式也学会了自己排查报错而不是一上来就复制代码。你顺着这套代码去改动、去扩展比如加一个座位等级的筛选、加一个票价区间的滑块、加一个订票成功后的弹窗动画这些功能往这个框架里填就是水到渠成的事。我个人在实际操作中最强烈的体会是这类项目最大的价值不在于页面多好看而在于它逼着你去想清楚数据在页面上是怎么流动的。想明白了这一点后面学框架就是另外一回事了。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门