资讯动态

火车票订票系统前端实战:从数据模型到交互闭环的完整实现

发布时间:2026/10/17 11:24:19 来源:尧图企业网站定制
简介这是一套面向Web开发初学者与课程设计学习者的火车票订票系统完整源码基于HTML、CSS与JavaScript三大前端技术并结合Servlet、JSP等Java Web组件实现前后端联动可用于理解在线订票平台的查询、登录注册与后台管理等典型业务场景。压缩包共405个文件约7.46MB其中png、gif、jpg等图片资源用于页面装饰与图标css与js文件负责样式布局和表单校验、AJAX异步请求等交互逻辑java、class、jsp及jar文件构成服务端处理与动态页面生成部分另含sql数据库脚本用于存储车次、站点、票务与用户数据。资源已有461人学习下载适合作为课程设计或毕业设计的参考案例。读者可从中获取从界面搭建到数据库操作的完整实现思路对照前端页面、后端请求处理与数据表结构进行拆解学习快速掌握Web项目的基本开发流程与模块划分方式。1. 火车票订票系统从一张静态页面到能跑通的交互闭环很多人第一次做「火车票订票系统」的网页设计交出来的东西长这样一个搜索框、几张车次卡片、一个「立即预订」按钮点下去毫无反应。页面看着挺像回事但一碰就露馅——这其实是把订票系统做成了海报。真正能拿得出手的版本核心不在视觉多花哨而在「查询 → 选座 → 填乘客 → 确认订单」这条链路能不能闭环。HTML 负责骨架CSS 负责布局与状态反馈JavaScript 负责数据流转和校验三者缺一不可。这篇笔记面向的是正在做课程设计、练手项目或者想补前端基本功的开发者我会把车次数据结构、座位选择逻辑、订单状态管理这些容易含糊的地方拆开讲参数怎么设、坑在哪都落到能直接抄的代码上。做完之后你手里应该是一个能真实交互、刷新不丢数据、边界情况有兜底的订票页面而不是一张会动的图。2. 先把车次数据模型定下来字段设计错了后面全是返工订票系统的复杂度八成来自数据模型没设计好。我见过太多人一上来就写 HTML 结构车次信息直接硬编码在标签里等到要做「按出发站筛选」「按日期查余票」的时候发现根本没法遍历和过滤只能推倒重来。所以第一步不是画页面是把车次、座位、订单这三类数据的字段想清楚。2.1 车次对象该包含哪些字段一个车次train至少要有车次编号、出发站、到达站、出发时间、到达时间、历时、各席别余票与票价。席别常见的是商务座、一等座、二等座有的线路还有无座。余票和票价建议做成嵌套对象而不是平铺成price1、price2这种否则后面加席别会非常痛苦。// 车次数据模型字段命名保持语义化方便后续筛选和渲染 const trains [ { id: G1001, // 车次编号唯一标识 from: 北京南, // 出发站 to: 上海虹桥, // 到达站 departTime: 08:00, // 出发时间HH:mm 格式 arriveTime: 12:28, // 到达时间 duration: 4小时28分, // 历时直接存字符串省去格式化 seats: { business: { price: 1748, remain: 3 }, // 商务座 first: { price: 933, remain: 12 }, // 一等座 second: { price: 553, remain: 45 } // 二等座 } }, // ... 更多车次 ];这里有个关键决策余票remain用数字而不是布尔值。因为订票要扣减库存布尔值只能表示「有/无」没法处理「只剩 2 张一次买 3 张」这种场景。价格用整数元避免浮点数在计算总价时出现0.1 0.2的精度问题。时间用字符串存储展示时直接输出只有需要计算跨天历时的时候才转成分钟数处理。2.2 座位与订单的关联方式座位选择有两种常见做法一种是「选席别不选具体座位」像真实购票那样只选二等座、系统分配另一种是「可视化选座」画出车厢座位图让用户点。课程设计里后者视觉冲击强但实现成本高而且座位图数据量大。我一般建议先做前者把订单流程跑通再考虑加座位图。订单对象要记录订单号、车次 id、席别、乘客信息、数量、总价、下单时间、状态。状态用枚举字符串比如pending、paid、cancelled不要用 0/1/2 这种魔法数字否则三个月后你自己都忘了 2 代表什么。// 订单模型状态用可读字符串方便调试和扩展 function createOrder(trainId, seatType, passengers) { const train trains.find(t t.id trainId); const seat train.seats[seatType]; return { orderId: ORD Date.now(), // 时间戳做订单号简单够用 trainId, seatType, passengers, // 乘客数组 count: passengers.length, totalPrice: seat.price * passengers.length, status: pending, // pending / paid / cancelled createdAt: new Date().toISOString() }; }乘客信息至少要有姓名和证件号。证件号校验别写太复杂课程设计里做个长度和格式的基础校验就行真实身份证校验算法涉及校验位计算可以单独抽成一个函数但别让它阻塞主流程。2.3 用 localStorage 做数据持久化刷新页面数据就没了是新手项目最容易被挑毛病的地方。用localStorage存订单列表是最低成本方案。注意存进去之前要JSON.stringify取出来要JSON.parse并且要处理解析失败的情况——用户手动改过存储、或者存了旧版本格式都会让parse抛异常。// 订单持久化读写都做异常兜底避免脏数据导致页面白屏 const ORDER_KEY train_orders; function saveOrders(orders) { try { localStorage.setItem(ORDER_KEY, JSON.stringify(orders)); } catch (e) { console.error(订单保存失败, e); // 存储满或隐私模式会抛错 } } function loadOrders() { try { const raw localStorage.getItem(ORDER_KEY); return raw ? JSON.parse(raw) : []; } catch (e) { console.warn(订单数据损坏已重置); return []; // 解析失败返回空数组不阻断渲染 } }参数说明ORDER_KEY用固定字符串别用动态 key否则每次读的都不是同一份数据。try/catch不是可选项隐私模式下localStorage可能直接不可用不兜底就是白屏。存之前建议给订单数组做个长度上限比如只保留最近 50 条避免长期使用撑爆存储配额。3. 查询与筛选让车次列表真正跟着条件动起来数据模型定好之后页面上的车次列表就不该是写死的 HTML而应该由 JavaScript 根据查询条件动态渲染。这一章解决「输入出发站、到达站、日期点查询列表正确刷新」这个核心交互顺带把空结果、无余票这些边界情况处理掉。3.1 查询条件的读取与校验查询表单通常有三个输入出发站、到达站、出发日期。读取值用document.querySelector拿但要注意用户可能什么都不填就点查询。我的做法是出发站和到达站必填日期可以默认今天。校验不通过时不要用alert弹窗那体验太糙在表单下方用一个提示区域显示文字。!-- 查询表单结构用 form 包裹方便统一处理提交 -- form idsearchForm classsearch-bar input typetext idfromStation placeholder出发站 / input typetext idtoStation placeholder到达站 / input typedate iddepartDate / button typesubmit查询/button p idsearchTip classtip/p /form// 查询逻辑先校验再过滤最后渲染 document.getElementById(searchForm).addEventListener(submit, function (e) { e.preventDefault(); // 阻止表单默认提交刷新页面 const from document.getElementById(fromStation).value.trim(); const to document.getElementById(toStation).value.trim(); const tip document.getElementById(searchTip); if (!from || !to) { tip.textContent 请填写出发站和到达站; return; } if (from to) { tip.textContent 出发站和到达站不能相同; return; } tip.textContent ; const result trains.filter(t t.from from t.to to); renderTrainList(result); });e.preventDefault()这行是血泪经验不写的话表单会以 GET 方式提交页面直接刷新你写的所有 JS 状态全丢。trim()处理用户输入前后空格不然「北京南 」和「北京南」匹配不上。过滤用filter而不是forEach里push代码更短且不易出错。3.2 车次卡片的动态渲染渲染函数接收过滤后的车次数组遍历生成 HTML 字符串再一次性塞进容器。这里有个性能细节不要在循环里反复操作 DOM拼好字符串再赋值innerHTML几十条数据下差异不明显但习惯要养好。// 渲染车次列表空结果要有兜底提示 function renderTrainList(list) { const container document.getElementById(trainList); if (list.length 0) { container.innerHTML p classempty没有找到符合条件的车次/p; return; } const html list.map(train { // 席别按钮余票为 0 时禁用用 disabled 属性控制 const seatButtons Object.entries(train.seats).map(([type, seat]) { const label { business: 商务座, first: 一等座, second: 二等座 }[type]; const disabled seat.remain 0 ? disabled : ; return button classseat-btn>// 事件委托父容器监听动态生成的按钮也能响应 document.getElementById(trainList).addEventListener(click, function (e) { const btn e.target.closest(.seat-btn); if (!btn || btn.disabled) return; // 不是席别按钮或已禁用则忽略 const trainId btn.dataset.train; const seatType btn.dataset.seat; openBookingPanel(trainId, seatType); // 打开订票面板 });closest从点击目标向上找最近的匹配元素比e.target.classList.contains更稳因为按钮里可能还有文字节点。btn.disabled判断不能省虽然 disabled 按钮通常不触发 click但不同浏览器行为有差异显式判断更保险。dataset.train对应 HTML 里的>// 打开订票面板填充车次与席别信息 let currentSelection null; // 记录当前选择下单时要用 function openBookingPanel(trainId, seatType) { const train trains.find(t t.id trainId); const seat train.seats[seatType]; currentSelection { trainId, seatType, price: seat.price }; document.getElementById(panelTrainId).textContent train.id; document.getElementById(panelRoute).textContent ${train.from} → ${train.to}; document.getElementById(panelSeat).textContent { business: 商务座, first: 一等座, second: 二等座 }[seatType]; document.getElementById(panelPrice).textContent seat.price; document.getElementById(bookingPanel).style.display block; renderPassengerInputs(1); // 默认显示 1 个乘客输入框 }currentSelection用模块级变量存当前选择比每次从 DOM 里反读更可靠。面板里的车次信息用textContent而不是innerHTML因为这些都是纯文本用innerHTML有 XSS 风险——虽然数据是自己定义的但习惯要正。4.2 乘客数量增减与总价实时计算乘客数量用「加/减」按钮控制最少 1 人最多不超过该席别余票。每次增减都重新渲染乘客输入框并更新总价。总价 单价 × 乘客数显示在面板底部。// 乘客数量管理增减时同步更新输入框和总价 let passengerCount 1; function changePassenger(delta) { const max trains.find(t t.id currentSelection.trainId) .seats[currentSelection.seatType].remain; const next passengerCount delta; if (next 1 || next max) return; // 边界拦截 passengerCount next; renderPassengerInputs(passengerCount); updateTotalPrice(); } function updateTotalPrice() { const total currentSelection.price * passengerCount; document.getElementById(totalPrice).textContent total; } function renderPassengerInputs(count) { const box document.getElementById(passengerBox); let html ; for (let i 0; i count; i) { html div classpassenger-row input typetext classp-name placeholder乘客${i 1}姓名 / input typetext classp-id placeholder证件号 / /div; } box.innerHTML html; }边界拦截next 1 || next max是关键不拦的话用户能减到 0 人或者加到超过余票下单时才发现问题就晚了。renderPassengerInputs每次全量重绘简单直接代价是已填内容会丢——如果用户填了 2 人再加到 3 人前两个输入框内容会被清空。要避免这个体验问题可以改成只追加/删除最后一个输入框但代码复杂度上升。课程设计里全量重绘可接受真实项目建议做增量。4.3 下单校验与订单写入点「确认下单」时要做三件事校验每个乘客的姓名和证件号非空、扣减对应席别余票、生成订单并存入 localStorage。校验不通过要明确指出是第几个乘客缺什么别只说「请填写完整」。// 确认下单校验 → 扣库存 → 存订单 → 刷新列表 document.getElementById(confirmBtn).addEventListener(click, function () { const names [...document.querySelectorAll(.p-name)]; const ids [...document.querySelectorAll(.p-id)]; for (let i 0; i names.length; i) { if (!names[i].value.trim()) { alert(请填写乘客${i 1}的姓名); return; } if (!ids[i].value.trim()) { alert(请填写乘客${i 1}的证件号); return; } } const passengers names.map((n, i) ({ name: n.value.trim(), idCard: ids[i].value.trim() })); // 扣减余票 const train trains.find(t t.id currentSelection.trainId); train.seats[currentSelection.seatType].remain - passengers.length; // 生成并保存订单 const order createOrder(currentSelection.trainId, currentSelection.seatType, passengers); const orders loadOrders(); orders.unshift(order); // 新订单放最前 saveOrders(orders); alert(订票成功订单号 order.orderId); document.getElementById(bookingPanel).style.display none; renderTrainList(trains); // 重渲染余票数字更新 renderOrderList(); // 刷新订单列表 });unshift把新订单放数组头部订单列表渲染时最新的在最上面符合直觉。扣库存要在生成订单之前顺序反了万一存订单失败库存就白扣了。重渲染车次列表用完整的trains数组因为余票变了之前筛选的结果可能不再准确简单起见直接全量刷新。5. 避坑与排查订票系统里最容易翻车的五个地方这一章是我自己踩过和帮别人看代码时反复遇到的坑每条按「现象 → 原因 → 解决」写你对着排查能省不少时间。5.1 点击查询后页面闪一下数据全没了现象填好条件点查询页面刷新输入框清空列表回到初始状态。原因form的默认提交行为没被阻止浏览器以 GET 方式提交并刷新页面。解决在submit事件处理函数第一行加e.preventDefault()。注意监听的是submit事件而不是按钮的click因为用户按回车也会触发提交只监听 click 会漏掉回车场景。5.2 动态生成的按钮点了没反应现象车次列表渲染出来后席别按钮点击无任何效果。原因事件监听器绑在了按钮本身上但按钮是后来用innerHTML生成的绑定时按钮还不存在。解决改用事件委托把监听器绑在始终存在的父容器trainList上通过e.target.closest(.seat-btn)判断点击目标。这是动态内容绑事件的通用解法记住「绑父不绑子」。5.3 刷新后订单列表空了现象下单成功能看到订单一刷新页面订单全消失。原因订单只存在内存变量里没写进localStorage或者写了但读取时 key 不一致。解决确认saveOrders和loadOrders用的是同一个ORDER_KEY并且页面加载时主动调用一次renderOrderList()从存储读取。另外检查JSON.parse是否被try/catch包住解析失败时返回空数组而不是抛异常中断后续代码。5.4 余票扣减后数字对不上现象买了 2 张票余票从 45 变成 44 而不是 43。原因扣减时用了- 1而不是- passengers.length或者扣减逻辑写在了遍历乘客的循环里每个乘客扣一次但基数算错。解决扣减语句只写一次数量用passengers.length。扣减后立即重渲染列表用渲染结果验证数字别靠脑补。5.5 证件号输入框能填进奇怪字符现象证件号里出现字母、符号甚至表情下单也不报错。原因只做了非空校验没做格式校验。解决加一个基础的正则校验比如身份证 18 位末位可能是 X护照按字母数字组合。校验失败时提示具体原因别只说「格式错误」。正则别写太严课程设计里能拦住明显非法输入即可真实校验规则复杂得多。// 证件号基础校验身份证或护照按需调整 function isValidIdCard(value) { const idPattern /^\d{17}[\dXx]$/; // 18 位身份证 const passportPattern /^[A-Za-z0-9]{6,15}$/; // 护照6-15 位字母数字 return idPattern.test(value) || passportPattern.test(value); }6. 进阶技巧用状态驱动渲染替代手动 DOM 操作前面几章的写法是「数据变了就手动调渲染函数」能跑通但车次列表、订单列表、面板状态各自为政改一处容易漏另一处。进阶做法是引入一个极简的状态对象所有变化都先改状态再统一调一个render()让页面成为状态的投影。这不是要你上框架而是理解「状态驱动」这个思路几十行代码就能实现。// 极简状态驱动所有 UI 都是 state 的函数 const state { keyword: { from: , to: }, // 当前查询条件 filtered: trains, // 当前展示的车次 orders: loadOrders(), // 订单列表 panel: null // 当前订票面板选择null 表示关闭 }; function render() { renderTrainList(state.filtered); renderOrderList(state.orders); renderPanel(state.panel); } // 查询只改状态不直接碰 DOM function search(from, to) { state.keyword { from, to }; state.filtered trains.filter(t t.from from t.to to); render(); } // 下单改状态后统一渲染 function placeOrder(order) { state.orders.unshift(order); saveOrders(state.orders); state.panel null; render(); }这样改的好处是任何操作都只关心「状态怎么变」不用记「该调哪几个渲染函数」。新增一个展示区域时只要在render()里加一行所有触发点自动生效。调试也简单把state打印出来页面该长什么样一目了然。验证方法上我习惯做三组检查第一空状态——不填条件查询、订单为空时页面是否正常第二边界——余票 1 张时买 2 张是否被拦、乘客数减到 0 是否被拦第三持久化——下单后刷新、关掉标签页再打开订单是否还在。这三组过了基本就稳了。最后说个我自己的习惯每次改完逻辑先把localStorage清空再测一遍完整流程。因为开发过程中存的脏数据经常掩盖真实问题清空重来能暴露「首次使用」路径上的 bug。这个习惯帮我省过很多次「明明本地好好的别人一打开就报错」的尴尬。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑