
简介这是一套面向小程序前端开发者与初学者的酒店预订平台静态模板完整覆盖首页、酒店列表、详情、筛选、城市选择、订单列表、个人中心等七个核心页面便于快速搭建Demo或临摹页面布局。资源共80个文件以wxml、wxss、js、json等小程序源码文件为主搭配png/gif/jpg图片资源及md说明文档整体仅1.35MB结构精简、易于按需修改。目前已有1046人学习下载适合需要快速理解小程序页面组织与样式写法的读者。页面均采用静态数据驱动切换自然目录按功能模块划分既能作为课程设计或毕业设计的前端原型也可在此基础上替换数据、对接接口直接扩展为真实可用的酒店预订小程序。1. 酒店预订小程序前端静态模板七个页面把交易前链路铺完了一套只有七个页面的酒店预订小程序前端静态模板能覆盖用户从打开小程序到下单前的大部分操作路径。这套源码里首页、搜索、筛选、详情、订单列表、个人中心、城市选择七张页面各自独立成目录pages 下直接按页面名展开没有把所有逻辑塞进一个大 index 里结构上比很多 demo 级小程序源码要干净。对于刚学完小程序基础、想看真实项目怎么组织文件的前端开发者来说这套代码比空谈组件化更有参考价值对于需要快速搭酒店预订类原型的开发者静态模板意味着拿到手就能改页面、跑通交互不用从零画布局。另外它没有后端依赖运行起来只需要一个微信开发者工具这对验证页面流非常友好。2. 从 app.json 和目录结构拆七个页面的路由骨架2.1 pages 数组的注册顺序决定了首屏是谁小程序里所有页面都必须注册在 app.json 的 pages 字段数组第一项就是启动后看到的第一个页面。这套源码里 homePage 放在了最前面意味着首页是入口后续从首页通过 wx.navigateTo 跳到搜索、城市选择、详情等页面。注册顺序还会影响开发者工具编译后的页面层级建议把主路径页面往前放次要页面靠后和用户访问频率保持一致。{ pages: [ pages/homePage/homePage, pages/select_city/select_city, pages/searchHotel/searchHotel, pages/hotelFilter/hotelFilter, pages/hotelDetail/hotelDetail, pages/orderList/orderList, pages/minePage/minePage ], window: { navigationBarTitleText: 酒店预订, navigationBarBackgroundColor: #1b7af0, navigationBarTextStyle: white }, usingComponents: {} }pages 数组里每个元素都是页面 JS 文件路径去掉扩展名的写法页面目录名和文件名保持一致能少踩很多坑。window 里的 navigationBarTitleText 是全局导航栏标题页面里如果自己配置了 navigationBarTitleText 会覆盖全局值。navigationBarBackgroundColor 只接受十六进制色值不能写 rgba这一点在调导航栏透明效果时容易忽略。以下是七个页面的路径与职责对照方便后续按图索骥页面路径页面角色核心职责pages/homePage/homePage首页搜索入口、推荐酒店位、banner 位pages/select_city/select_city城市选择热门城市、拼音索引、搜索城市pages/searchHotel/searchHotel搜索结果关键词搜索、入住/离店日期选择pages/hotelFilter/hotelFilter筛选页价格区间、星级、评分、品牌筛选pages/hotelDetail/hotelDetail酒店详情房型列表、图片、位置、预订入口pages/orderList/orderList订单列表全部/待付款/已入住/已完成标签切换pages/minePage/minePage个人中心登录卡片、订单入口、优惠券、设置这套页面里没有支付、没有登录态管理本质上是一条从选酒店到看订单的读链路。好处是每个页面都可以被独立打开调试坏处是页面之间传参完全依赖 URL query 和本地缓存后面接真实后端时得补一套全局状态管理。2.2 template.wxml 与 app.wxss页面复用的两个层级common 目录下的 template.wxml 放的是酒店卡片模板这是小程序早期比较常用的代码复用方式。首页、搜索结果页、订单列表里展示的酒店信息卡片如果共用一份模板改卡片的宽高、价格颜色、标签样式只需要动一处。template namehotelCard view classhotel-card>const request (options) { const baseUrl https://your-api.example.com; return new Promise((resolve, reject) { wx.request({ url: baseUrl options.url, method: options.method || GET, data: options.data || {}, header: { content-type: application/json, ...options.header }, success: (res) { if (res.statusCode 200 res.statusCode 300) { resolve(res.data); } else { reject(new Error(请求失败状态码 res.statusCode)); } }, fail: (err) reject(err) }); }); }; module.exports request;这里把 baseUrl 直接定义在文件内实际项目中这个值通常从全局配置读取或者根据环境变量区分开发、测试、生产地址。header 合并用了展开运算符调用方传入的 token、设备信息等 header 字段会覆盖默认值。成功回调里只按状态码 2xx 放行业务层的 code 字段留给页面自己判断这样的好处是 request 层只做传输层收敛不掺业务判断。静态模板阶段没有真实接口可以在 request.js 里加一层开发环境判断直接返回本地 mock 数据。比如判断const isMock true为 true 时从mock/目录 require 对应 JSON 并 resolve这样页面联调前就能先把渲染逻辑跑通。3.2 city.js 与 selectCity、clearInput 组件的配合city.js 是城市数据的静态字典里面通常是一组带拼音和热门标记的城市对象。select_city 页面加载后从 city.js 读取数据按首字母分组渲染右侧索引列表。这个设计的核心是数据与视图分离城市数据是静态的可以做成模块索引和滚动逻辑放页面组件里。const cityList [ { id: city_001, name: 北京, pinyin: beijing, hot: true }, { id: city_002, name: 上海, pinyin: shanghai, hot: true }, { id: city_003, name: 广州, pinyin: guangzhou, hot: false } ]; module.exports { cityList };城市数据里带 pinyin 字段是为了支持拼音首字母检索和索引定位hot 字段控制热门城市展示。选择城市时页面把选中的城市通过wx.setStorageSync(selectedCity, cityName)写入本地缓存首页在 onShow 里读取缓存并刷新搜索框的默认城市文案。这个链路不依赖后端静态模板里跑起来就是完整闭环。selectCity 组件和 clearInput 组件位于 components 目录。clearInput 是输入框的清空按钮用户点击后清空输入框内容并触发 bindclear 事件。这类小组件用Component构造器定义需要注意properties里的值默认是单向的组件内部改了不会自动同步回父组件要在页面上通过事件回调手动更新。3.3 commonUtils.js 与 util.js 的职责边界utils 目录下的 util.js 是微信开发者工具初始化项目时自带的工具模块里面一般放着格式化时间的函数。common 目录下的 commonUtils.js 是业务工具函数库两者分工不同util.js 偏通用commonUtils.js 偏业务。看源码时先分清这两个文件后面改逻辑不会改错地方。const formatPrice (price) { return Number(price).toFixed(2); }; const formatDate (timestamp, fmt YYYY-MM-DD) { const date new Date(timestamp); const o { YYYY: date.getFullYear(), MM: String(date.getMonth() 1).padStart(2, 0), DD: String(date.getDate()).padStart(2, 0) }; return fmt.replace(/YYYY|MM|DD/g, (match) o[match]); }; module.exports { formatPrice, formatDate };formatPrice 对价格做两位小数归一化防止后端返回 329 这种整数时前端显示成 ¥329 而不是 ¥329.00。formatDate 用 replace 逐个匹配年月日比 split 拼接更稳妥也方便扩展 HH:mm 格式。这类纯函数建议保持无副作用不依赖全局变量方便单测。4. 七个页面逐页拆解数据从哪来、绑到哪去4.1 homePage 首页搜索入口与推荐位布局homePage 是整套源码里信息密度最大的页面。顶部是城市选择和搜索框中间可能有轮播 banner下面一般是猜你喜欢或附近热门的酒店卡片列表。静态模板里这些数据直接写在 JS 文件的 data 字段里渲染由 wx:for 完成。Page({ data: { cityName: 北京, banners: [ { id: 1, image: /images/banner_1.png }, { id: 2, image: /images/banner_2.png } ], hotelList: [ { id: H01, name: 城市中心酒店, price: 329, thumb: /images/hotel_1.png, tags: [近地铁, 含早餐] }, { id: H02, name: 江景假日酒店, price: 458, thumb: /images/hotel_2.png, tags: [江景房, 免费停车] } ] }, onShow() { const city wx.getStorageSync(selectedCity); if (city) { this.setData({ cityName: city }); } } })data 里直接维护一份 hotelList 是静态模板的典型写法数组每一项的字段要和 hotelCard 模板里引用的属性严格对应。onShow 里读取本地缓存的城市名并覆盖默认值这是静态模板实现跨页数据同步最轻量的方式。注意 setData 传入的字段名如果是 cityNameWXML 里绑定{{cityName}}会自动更新不需要手动渲染。首页跳转搜索页要带上城市参数常见写法是这样goSearch() { wx.navigateTo({ url: /pages/searchHotel/searchHotel?city this.data.cityName }); }参数拼在 url 里SearchHotel 页面 onLoad 的 options 里就能拿到 city。中文参数在部分机型上可能需要 encodeURIComponent建议跳转前统一转义接收时再 decode。4.2 searchHotel 搜索页与 hotelFilter 筛选页的参数组装searchHotel 页面的职责是汇聚搜索条件关键词、城市、入住日期、离店日期、入住人数。静态版本里条件字段都存在 data 里点搜索后跳转到 hotelFilter 或者复用 hotelList 展示。筛选页从搜索页接收条件再叠加价格、星级、评分等筛选项。从搜索页携带多参数跳转时URL 会越拼越长这时可以先把参数整理成一个 query 对象再序列化buildQuery() { const { keyword, cityName, checkInDate, checkOutDate } this.data; const params { keyword, city: cityName, checkIn: checkInDate, checkOut: checkOutDate }; const query Object.keys(params) .filter((key) params[key]) .map((key) ${key}${encodeURIComponent(params[key])}) .join(); wx.navigateTo({ url: /pages/hotelFilter/hotelFilter?${query} }); }filter 方法把空值字段过滤掉避免 URL 上挂一堆空参数。encodeURIComponent 处理中文和特殊字符接收端用 options 读取时再 decodeURIComponent。hotelFilter 页面拿到这些条件后把选中筛选项拼成请求参数调 request.js 获取结果。静态模板阶段没有请求这一步可以改为从本地 mock 数据里做 filter 匹配。筛选页常见的坑是重置功能用户选完价格区间、星级、评分后点重置所有筛选字段要一次性清空不能只重置部分。建议把筛选初始值定义成一个常量对象重置时 setData 整个对象。4.3 hotelDetail 详情页的参数传递酒店详情页是用户决策的关键页面。静态模板里它的价值在于把房型列表、酒店设施、位置、用户评价这些模块的占位结构全部搭好。跳转时上一页传入酒店 id详情页用它去查询详情数据。Page({ data: { hotelId: , hotelDetail: null, roomList: [] }, onLoad(options) { const hotelId options.id || ; this.setData({ hotelId }); this.fetchHotelDetail(hotelId); }, fetchHotelDetail(id) { const detail mockHotelDetailMap[id]; this.setData({ hotelDetail: detail, roomList: detail ? detail.roomList : [] }); } })onLoad 里的 options 是页面跳转时 url 携带的所有 query 参数这是一个纯字符串对象需要做空值兜底。fetchHotelDetail 里用了 mock 映射表这是静态模板在等后端接口时的常见策略先定义一个以 id 为 key 的对象按 id 查详情。注意 setData 的 hotelDetail 为 null 时WXML 里访问{{hotelDetail.name}}不会报错会自动当作 undefined 处理但如果写了wx:if{{hotelDetail.name}}就需要注意空指针问题。4.4 orderList 与 minePage静态数据下的登录态模拟orderList 页面通过顶部 tab 切换订单状态静态数据里每个订单带一个 status 字段切换 tab 时用 status 过滤数组。这个逻辑在真实项目里通常走服务端分页查询但静态模板可以先用本地过滤跑通交互。const allOrders [ { orderId: 202501010001, hotelName: 城市中心酒店, status: pending, amount: 329 }, { orderId: 202501020002, hotelName: 江景假日酒店, status: paid, amount: 458 } ]; Page({ data: { tabs: [全部, 待付款, 已入住, 已完成], activeTab: 0, orderList: [] }, onLoad() { this.setData({ orderList: allOrders }); }, switchTab(e) { const index e.currentTarget.dataset.index; let filtered; if (index 0) { filtered allOrders; } else { const statusMap [, pending, checkedIn, completed]; filtered allOrders.filter((item) item.status statusMap[index]); } this.setData({ activeTab: index, orderList: filtered }); } })statusMap 把 tab 数组的下标翻译成业务状态值这个映射关系集中写在一个地方后面改状态枚举只动一处。minePage 里一般有登录卡片、我的订单入口、优惠券、联系客服等模块。静态模板通常用wx.getUserProfile模拟登录或者直接用写死的用户头像和昵称渲染。真正接后端时登录态建议改用 wx.login 拿 code 换 tokentoken 存在 Storage 里request.js 的 header 里统一注入。4.5 select_city 城市选择与本地检索的边界城市选择页的实现逻辑是右侧跳动的字母索引加左侧城市列表点击字母滚到对应分组。静态模板里没有第三方滑动库通常用 scroll-view 的 scroll-into-view 实现分组定位每个分组设置一个 id索引项绑定点击事件后设置 scrollIntoView 字段。城市数量固定在几百个以内时本地纯前端检索没有问题。一旦城市数据量达到上千级或者需要模糊搜索拼音、英文名、历史别名本地遍历就会卡顿这时候应该把搜索请求发到服务端走 Elasticsearch 或 MySQL 的 like 索引。静态模板里用本地 filter 是合理方案但要意识到它的性能边界。5. 把静态模板改成能联调的动态项目三处必改5.1 给 request.js 加一道 mock 开关静态模板在接真实接口前很多页面已经依赖本地 mock 数据跑通了展示逻辑。合理的方式是让页面统一走 request.js而不是跳过后端直连全局 JSON。在 request.js 里加一个 mock 判定开发环境直接返回本地数据切生产环境只改一个变量页面代码不用动。const isMock true; const request (options) { if (isMock) { return new Promise((resolve) { const mockData require(../mock/ options.url .js); setTimeout(() resolve(mockData), 200); }); } return realRequest(options); };mock 文件名与 url 对应比如请求/hotelListmock 目录下放 hotelList.js约定简单直接。setTimeout 模拟网络延迟便于观察 loading 状态是否正常。接真实接口时把 isMock 改成 false并且保证 mock 数据字段和真实返回字段完全一致否则页面会出现字段缺失导致的空白。5.2 setData 的粒度与生命周期时机静态模板页面数据量小setData 一次传一整个对象没有明显问题。但页面里如果出现大数组就必须注意粒度了。setData 每次调用都会把数据从逻辑层传到渲染层高频调用或传输大量数据会直接体现在页面帧率上。优化方式有三种一是只更新变化的字段比如修改某条酒店的 price用setData({ hotelList[2].price: 399 })而不是重传整个数组二是把与渲染无关的数据放在 data 外面比如翻页用的 pageIndex、请求状态标记用普通属性存即可三是列表采用分页加载而不是一次渲染全部静态模板里 20 条以内的数据看不出问题超过 50 条就能明显感知滚动卡顿。生命周期这里有一个容易忽略的时序问题onLoad 里发出的异步请求可能在 onReady 之后才返回如果页面初始化时依赖请求结果做一些 canvas 绘制或组件初始化操作就会拿不到数据。正确的做法是把依赖数据的初始化逻辑放在回调里执行而不是放在 onReady 里提前执行。5.3 一个排错习惯在 WXML 里临时 dump 数据静态模板改动态数据的阶段经常出现页面没报错但就是白屏的情况。这时候检查顺序应该是先看 data 里有没有值再看 WXML 绑定路径对不对。最快的方式是直接在页面上放一段临时的调试文本把可疑数据完整输出。view styledisplay:none{{JSON.stringify(hotelList)}}/view用 display:none 包住这个调试 view正常渲染时不可见但可以在开发者工具的 WXML 面板里直接展开查看 hotelList 的完整结构。如果这里能渲染出来说明数据没问题问题出在下层的字段绑定上如果这里是空数组说明数据源就没写对往上一层查请求或 mock 返回。看完记得删掉别留在正式代码里。调试完字段后还有一个值得养成的习惯给列表渲染加 wx:key。wx:key 的值建议用数据里的唯一 id而不是索引*this或 index。用 index 作为 key列表项位置变化时小程序会错误地复用组件状态导致输入框内容错位或图片闪烁。静态模板里数据结构简单可能感受不到一旦加了删除、排序这类操作key 的作用就会立刻体现出来。本文还有配套的精品资源点击获取