简介:这是一套基于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 type="text" list="city-list" id="from-city" placeholder="请输入出发城市"> <datalist id="city-list"> <option value="北京"></option> <option value="上海"></option> <option value="广州"></option> </datalist>出发日期用type="date"的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 []; } }这种健壮性处理也是一个专业开发者必备的意识。项目做到这个程度,就不是简单的大作业了,而是带一点工程思维的完整前端应用。
做完了这个火车票订票系统,你会发现自己在好几个方面都有明显进步:不再害怕数组对象的各种操作,理解了数据驱动视图的思考方式,也学会了自己排查报错而不是一上来就复制代码。你顺着这套代码去改动、去扩展,比如加一个座位等级的筛选、加一个票价区间的滑块、加一个订票成功后的弹窗动画,这些功能往这个框架里填就是水到渠成的事。我个人在实际操作中最强烈的体会是,这类项目最大的价值不在于页面多好看,而在于它逼着你去想清楚数据在页面上是怎么流动的。想明白了这一点,后面学框架就是另外一回事了。
本文还有配套的精品资源,点击获取