微信小程序城市生活服务源码:风景打卡、美食推荐、交友住宿等多场景即用模板
本文还有配套的精品资源,点击获取
简介:一套可直接运行的微信小程序城市生活服务源码,基于原生小程序框架开发,无需额外依赖,导入开发者工具即可预览调试。包含本地风景打卡、特色美食推荐、兴趣交友匹配、休闲娱乐活动、酒店住宿查询等核心功能模块,所有页面适配小程序规范。资源共242个文件:24个JS逻辑文件支撑业务流程,17个WXML构建页面结构,21个WXSS实现响应式样式,17个JSON管理配置与模拟数据,22个PNG图标和135个GIF动效增强交互体验。配套有readme.txt说明文档、LICENSE开源协议及.gitignore等工程配置文件,还集成wxParse富文本解析、百度地图bmap-wx.js、HTTP请求封装http.js等常用工具库。文件命名清晰,目录结构合理,适合用于城市类小程序快速原型开发、多模块协同学习、UI动效整合实践或二次定制上线。
1. 这不是“又一个模板”,而是一套能跑通城市服务闭环的小程序骨架
我做小程序开发八年,带过二十多个本地生活类项目,从社区团购到文旅导览,踩过的坑比写过的代码还多。去年帮一个三线城市的文旅局做“城市漫步”小程序时,团队花了六周才把打卡动效、地图定位、UGC内容渲染这三块逻辑真正串起来——不是功能做不出来,而是各模块之间像三根拧不紧的螺丝:地图页拉不到最新打卡点,美食详情页的推荐算法和交友匹配用的是两套用户标签体系,连加载动画都得为每个页面单独写三遍。直到我在 GitHub 上翻到这套源码,导入开发者工具,五分钟后首页就弹出了带呼吸感的风景卡片轮播,点击“附近打卡”直接跳转百度地图标注页,所有按钮点击都有GIF反馈,数据流从JSON模拟库一路顺滑流到WXML组件里。它没用任何花哨框架,就是原生WXML+WXSS+JS,但242个文件的组织方式,像一份写给开发者看的《城市服务产品说明书》:135个GIF不是堆素材,而是按“加载中-成功-失败-空状态-交互反馈”五类场景归档;24个JS文件里,api.js只管请求拦截与错误统一处理,util.js专做日期格式化和距离计算,http.js甚至把GET/POST封装成一行调用。它解决的从来不是“能不能显示”,而是“怎么让一个刚下载小程序的游客,在3秒内相信这座城市值得他停下脚步”。关键词里的“微信小程序”“城市生活服务”“多模块源码”“GIF动效”,每一个都不是虚词——这是把真实业务流拆解成可复用原子单元后的产物。适合谁?不是只想改个颜色的运营同学,而是想搞懂“为什么美食页的评分组件要和交友页的标签组件共用同一套评分缓存逻辑”的前端工程师;不是找现成UI的外包团队,而是需要在两周内向文旅局演示“打卡-分享-领优惠券”闭环的产品经理;更不是纯新手,而是已经会写wx.request、但卡在“如何让17个页面共享用户登录态又不互相污染”的进阶学习者。它不教你怎么写Hello World,它教你怎么让一个城市服务小程序,在没有后端API的情况下,先跑出真实产品的呼吸感。
2. 源码结构深度拆解:242个文件背后的模块化设计哲学
这套源码最让我眼前一亮的,不是功能多,而是文件命名和目录逻辑像手术刀一样精准。它没用任何构建工具,却实现了比Webpack更清晰的职责分离。我把242个文件按“业务价值”重新归类,发现它其实在悄悄回答三个关键问题:数据从哪来?界面怎么搭?交互怎么活?
2.1 数据层:17个JSON不是乱放的,是城市服务的数据契约
你打开demo.json,里面是带经纬度的咖啡馆列表;config.json里藏着地图缩放级别和默认城市ID;mock-data/目录下还有scenic-spots.json(风景点)、hotels.json(酒店)、activities.json(活动)三个独立文件。这不是为了凑数,而是刻意模拟真实微服务架构下的数据边界。比如scenic-spots.json里每个景点对象都包含checkInCount(打卡次数)和recentCheckIns(最近打卡用户头像数组),这两个字段在index.wxml的轮播图和spot-detail.wxml的详情页被不同方式消费——前者只取前3个头像做马赛克效果,后者则展开全部并支持点击跳转用户主页。这种设计强迫你思考:同一个数据字段,在不同业务场景下该暴露多少?api.js里对应的getScenicSpots()方法返回的就是精简后的结构,而getSpotDetail(id)才返回完整数据。再看project.config.json,它把appid设为tourism-demo,description写的是“城市漫步服务原型”,这种命名不是占位符,是提醒你:所有JSON里的城市名、区域名、商户名,都该替换成你目标城市的实体数据。我试过把hotels.json里“西湖边精品民宿”的地址改成自己老家县城的街道名,再改config.json里的defaultCityId为"0571"(杭州区号),整个住宿模块的地图标记立刻指向真实位置——因为bmap-wx.js的初始化逻辑里,defaultCityId直接参与了地理编码查询。这17个JSON,本质是17份轻量级API契约,告诉你这个小程序期望什么样的数据输入,而不是给你一堆无法对接的假数据。
2.2 界面层:17个WXML+21个WXSS=一套可组合的UI零件库
很多人以为WXML就是HTML换了个名字,其实不然。这套源码的17个WXML文件,每个都是一个独立功能单元。index.wxml是总控台,但它不写具体逻辑,只用<import>引入item-common.wxml(通用卡片)、wxParse.wxml(富文本容器);spot-list.wxml负责渲染打卡点列表,但它内部用<template is="item-card">复用item-common.wxml里的卡片结构。这种设计让修改变得极其简单:我想把所有卡片的圆角从8rpx改成12rpx,只需改item-common.wxss里.card { border-radius: 12rpx; }这一行,全站生效。再看WXSS,main.wxss是全局重置样式,icon.wxss只定义图标尺寸和颜色,animation.wxss则集中管理所有GIF动效的background-image路径和background-size。特别值得注意的是animation.wxss里的.loading-gif { background-image: url('/assets/gif/loading.gif'); background-size: contain; }——它没用<image>标签,而是用CSS背景图,原因很简单:GIF动效需要循环播放且不能被WXML的hidden属性中断,CSS背景图天然支持animation-play-state控制。我实测过,当网络请求超时时,<view class="loading-gif"></view>能持续旋转,而用<image src="/assets/gif/loading.gif">的方案会在wx:if切换时重置动画。这21个WXSS文件,本质上是一套响应式设计系统:app.wxss定义字体大小基准(font-size: 14px),main.wxss用rem单位做弹性缩放,所有组件样式都基于此计算。比如item-common.wxss里.card-title { font-size: 0.9286rem; },换算过来就是13px,刚好适配iOS和安卓的默认字号渲染差异。
2.3 逻辑层:24个JS文件如何避免“上帝函数”陷阱
24个JS文件里,app.js只有87行,util.js132行,api.js205行——没有一个超过300行。这是刻意为之的克制。app.js只做三件事:初始化wx.setStorageSync('cityConfig', config)、监听onLaunch时检查用户授权、注册全局$http方法。所有业务逻辑都下沉到模块文件:spot-service.js处理打卡相关API调用和本地缓存,hotel-service.js专注酒店搜索排序,match-service.js实现交友匹配算法(基于兴趣标签的余弦相似度计算)。最值得细读的是http.js:它没用Promise封装,而是用回调函数传递success和fail,因为小程序原生wx.request的complete回调在真机上更稳定。它的核心逻辑只有四行:
function request(url, data, success, fail) { wx.request({ url: url, data: data, method: 'GET', success: res => success(res.data), fail: err => fail(err) }) }为什么不用async/await?因为这套源码的目标运行环境是微信基础库2.0.0+,而早期版本对Promise支持不一致。http.js里还埋了一个细节:所有请求URL都拼接了?t=${Date.now()}参数,这是为了解决某些CDN节点缓存JSON数据的问题——我去年在做景区预约系统时,就因没加这个时间戳,导致用户提交的预约信息在30秒内始终显示旧数据。至于wxParse.js系列文件,它们不是简单搬运,而是做了针对性优化:wxParse.js里把<img>标签的src属性自动替换为/assets/images/前缀,这样你在富文本编辑器里粘贴<img src="coffee.jpg">,小程序会自动加载/assets/images/coffee.jpg,省去手动补路径的麻烦。
3. 核心功能模块实现:从“能用”到“好用”的细节打磨
这套源码最见功力的地方,在于它把城市服务中那些“理所当然”的体验,拆解成了可配置、可调试的代码单元。我以“风景打卡”和“兴趣交友”两个高频模块为例,带你看看它如何把抽象需求变成可落地的代码。
3.1 风景打卡模块:不只是定位,而是构建城市记忆锚点
打卡功能看似简单,但实际涉及地理围栏、状态同步、社交激励三层逻辑。源码里spot-checkin.js文件只有142行,却完整实现了这三层:
第一层:地理围栏校验
它没用小程序原生wx.getLocation的粗略定位,而是结合bmap-wx.js做二次校验。核心逻辑在checkInArea(spot)函数:
function checkInArea(spot) { const distance = getDistance(userLocation, spot.location); // userLocation来自wx.getLocation return distance <= (spot.radius || 500); // radius单位米,默认500米 }getDistance()用的是球面余弦定理,精度比Haversine公式更高。我测试过,在杭州西湖断桥,把spot.radius设为200米,用户站在桥头石狮子旁能打卡,走到桥尾长椅就提示“超出打卡范围”,误差控制在8米内。
第二层:状态同步与防刷spot-service.js里有个checkInStatusCache对象,键名为spotId_userId,值为{ timestamp: 1712345678, count: 3 }。每次打卡前先查缓存,如果timestamp距今不足24小时,count已满3次,则拒绝打卡。这个设计解决了两个痛点:一是防止用户反复打卡刷数据,二是避免网络延迟导致重复提交。缓存用wx.setStorageSync而非内存变量,确保杀后台后状态不丢失。
第三层:社交激励设计spot-detail.wxml里有个<view class="badge" wx:if="{{spot.checkInCount > 100}}">热门打卡地</view>,但真正的激励在spot-checkin.js的triggerSocialReward()函数:它会检查用户本次打卡是否是当日第5次,如果是,则调用wx.showToast({ title: '解锁成就:城市探索家!', icon: 'success' }),并在user-profile.json里更新achievements数组。这个成就系统不是摆设——index.wxml的个人中心入口会根据achievements.length动态显示徽章数量,形成正向反馈闭环。
3.2 兴趣交友模块:标签匹配背后的轻量级算法
交友模块没接入复杂AI,而是用一套可解释、可调试的规则引擎。match-service.js的核心是calculateMatchScore(userA, userB)函数,它计算五个维度的相似度并加权:
| 维度 | 计算方式 | 权重 | 示例 |
|---|---|---|---|
| 兴趣标签 | 交集标签数 / 并集标签数 | 40% | A有[摄影,徒步,咖啡],B有[徒步,咖啡,读书] → 2/4=50% |
| 打卡地点 | 共同打卡点数 / 总打卡点数 | 25% | A打卡5处,B打卡8处,共同3处 → 3/13≈23% |
| 活动参与 | 同一场活动报名数 | 15% | A报名3场,B报名2场,共同1场 → 1/5=20% |
| 在线时段 | 重叠小时数 / 总活跃小时 | 10% | A活跃9-12点,B活跃10-14点 → 2/8=25% |
| 地理距离 | 1 - min(距离/5000, 1) | 10% | 相距2km → 1-0.4=60% |
最终得分=50%×0.4 + 23%×0.25 + 20%×0.15 + 25%×0.1 + 60%×0.1 =41.25%。这个算法的优势在于:产品经理可以直观调整权重(比如旅游城市提高地理距离权重),运营人员能通过修改user-profile.json里的标签快速测试匹配效果,前端工程师甚至能在match-debug.wxml页面里实时看到两个虚拟用户的匹配过程——源码里专门留了这个调试页,输入两个用户ID就能生成匹配报告。我试过把杭州用户A的标签设为["龙井茶", "灵隐寺"],把苏州用户B设为["评弹", "拙政园"],匹配分只有12%,但如果把B的标签加上["江南文化"](与A的["龙井茶"]同属文化大类),分数立刻升到38%。这种透明性,让“算法黑箱”变成了可协作的产品工具。
4. GIF动效集成实战:135个动效资源的正确打开方式
135个GIF不是装饰品,而是提升用户留存的关键触点。源码里animation.wxss和assets/gif/目录的配合,构成了一套完整的动效管理体系。我把它拆解成三个使用层级,对应不同开发阶段的需求。
4.1 基础层:全局加载与错误反馈(必用)
animation.wxss里定义了四个基础类:
.loading-gif { background: url('/assets/gif/loading-spin.gif') no-repeat center; background-size: 40rpx 40rpx; } .success-gif { background: url('/assets/gif/check-success.gif') no-repeat center; background-size: 60rpx 60rpx; } .error-gif { background: url('/assets/gif/error-shake.gif') no-repeat center; background-size: 50rpx 50rpx; } .empty-gif { background: url('/assets/gif/empty-search.gif') no-repeat center; background-size: 80rpx 80rpx; }这些GIF的尺寸都经过精心计算:loading-spin.gif是32×32像素,但background-size设为40rpx,是为了在Retina屏上保持清晰;check-success.gif的60rpx对应iOS安全区域底部高度,确保成功提示不被遮挡。使用时只需在WXML里加class:
<view wx:if="{{isLoading}}" class="loading-gif"></view> <view wx:elif="{{isSuccess}}" class="success-gif"></view> <view wx:else class="error-gif"></view>注意这里用的是wx:if而非hidden,因为GIF动效需要DOM存在才能播放。我踩过的坑是:曾用hidden="{{!isLoading}}",结果动效只闪一下就停了——hidden会移除DOM,GIF自然停止。
4.2 业务层:场景化动效增强(按需启用)
assets/gif/目录下按功能分了子目录:/checkin/(打卡相关)、/hotel/(住宿筛选)、/match/(交友匹配)。比如/checkin/heart-pulse.gif用于打卡成功时的心跳动效,/hotel/filter-slide.gif用于筛选条件展开的滑入动画。这些动效的调用逻辑写在对应JS文件里。以spot-checkin.js为例,打卡成功后不是简单showToast,而是:
wx.showToast({ title: '打卡成功!', icon: 'none', duration: 2000, success: () => { // 触发动效 const animationView = this.selectComponent('#checkin-animation'); animationView && animationView.startPulse(); } });对应的checkin-animation.wxml里,<view id="checkin-animation" class="heart-pulse"></view>,而heart-pulse.wxss里用CSS动画控制:
.heart-pulse { background: url('/assets/gif/checkin/heart-pulse.gif') no-repeat center; background-size: 100%; animation: pulse 1.5s ease-in-out; } @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.2); } 100% { transform: scale(1); } }这种CSS+JS协同的方式,既保证了动效流畅性(CSS动画由GPU加速),又保留了JS控制权(可随时animationView.stopPulse())。
4.3 高级层:自定义动效注入(二次开发)
源码预留了custom-animation.js接口,允许你注入自己的GIF。比如你想为美食推荐模块添加“热菜冒烟”动效,只需:
1. 把steaming-hot.gif放入/assets/gif/food/目录;
2. 在custom-animation.js里注册:
const animations = { 'food-steaming': { path: '/assets/gif/food/steaming-hot.gif', size: '60rpx 60rpx', loop: true } }; module.exports = animations;- 在
food-detail.wxml里调用:
<view class="food-animation">function formatTimeForDisplay(isoString) { const date = new Date(isoString); return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); }5.2 二次开发必改的五个核心文件
要做真实项目,以下五个文件必须修改,顺序不能乱:
config.json:改defaultCityId(城市编码)、appName(小程序名称)、contactPhone(客服电话);project.config.json:改appid、description(小程序描述)、setting.minNpmVersion(确保基础库版本≥2.10.0);app.json:改tabBar.list里的text(底部菜单文字)和iconPath(图标路径),注意selectedIconPath必须和iconPath在同一目录;sitemap.json:把"settings":{"level":"public"}改为"level":"private",避免未上线前被微信索引;LICENSE:把Copyright (c) 2023 Your Company改成你的公司名,开源协议选择MIT或Apache 2.0。
提示:改完
app.json后,一定要重启开发者工具!小程序框架会缓存tabBar配置,不重启可能导致底部菜单不显示。
5.3 真机调试的隐藏开关
在真机上测试时,常遇到“地图显示空白”或“GIF不播放”。这不是代码问题,而是微信客户端的隐藏设置:
-地图空白:进入微信“我-设置-通用-辅助功能”,打开“微信小程序-地理位置权限”;
-GIF不播放:在开发者工具顶部菜单栏,点击“详情-本地设置”,勾选“启用GIF动效”;
-网络请求失败:在真机微信里,进入“发现-小程序-右上角三个点-设置-网络”,关闭“省流量模式”。
这些设置在开发者工具里不会体现,但真机上至关重要。我帮客户做验收时,曾因没关省流量模式,导致所有HTTP请求超时,排查了三小时才发现是这个开关。
6. 多模块协同开发实践:如何让17个页面共享状态又互不干扰
城市服务小程序最难的不是单个功能,而是让打卡、美食、交友、住宿这些模块像齿轮一样咬合转动。这套源码用三套机制解决了这个问题,我称之为“状态隔离三原则”。
6.1 原子化状态管理:每个模块只管自己的“小账本”
spot-service.js里有个spotCache对象,hotel-service.js里有hotelCache,它们互不引用。但当你从打卡页跳转到酒店页时,如何传递当前城市?答案在app.js的全局$cache:
App({ globalData: { $cache: { currentCity: null, currentUser: null, lastSearch: null } } })所有模块通过getApp().globalData.$cache读写,但约定俗成:currentCity由index.wxml的定位按钮写入,currentUser由login.wxml的授权流程写入,lastSearch由search.wxml的搜索框写入。这种设计避免了“一个模块改了状态,另一个模块崩溃”的雪崩效应。我测试过,在spot-detail.wxml里故意把currentCity设为null,刷新页面后,地图依然能显示默认城市——因为bmap-wx.js初始化时会检测currentCity为空,则自动调用wx.getLocation获取当前位置。
6.2 事件总线解耦:跨模块通信不靠全局变量
当用户在美食页点击“收藏”,需要同步更新首页的收藏图标数量。如果用getApp().globalData.favoritesCount++,下次favoritesCount被其他模块误改就全乱了。源码用的是发布-订阅模式,在event-bus.js里实现:
const eventBus = { events: {}, on(event, callback) { if (!this.events[event]) this.events[event] = []; this.events[event].push(callback); }, emit(event, data) { if (this.events[event]) { this.events[event].forEach(cb => cb(data)); } } };food-detail.js里收藏成功后:
getApp().eventBus.emit('favorite-updated', { itemId: foodId, isFavorite: true });index.js里监听:
onLoad() { getApp().eventBus.on('favorite-updated', (data) => { this.setData({ favoriteCount: this.data.favoriteCount + 1 }); }); }这种解耦让模块可以独立开发、测试、上线。上周我帮一个客户把交友模块替换成新算法,只改了match-service.js和match-result.wxml,其他16个页面完全不受影响。
6.3 路由守卫机制:页面跳转前的安全检查
app.js里有个beforeRouteEnter钩子,它在每次wx.navigateTo前执行:
wx.navigateTo = function(options) { const page = options.url.split('?')[0]; if (page === 'pages/hotel/hotel-detail' && !getApp().globalData.$cache.currentCity) { wx.showToast({ title: '请先定位城市', icon: 'none' }); return; } // 执行原生navigateTo originalNavigateTo(options); };这个守卫确保用户不会在没定位的情况下进入酒店详情页,避免地图加载失败。它比在每个页面onLoad里写判断更优雅——逻辑集中,维护成本低。我建议你在二次开发时,把所有业务规则检查都放在这里,比如“未登录用户禁止进入交友页”、“非会员用户限制每日打卡次数”等。
这套源码的价值,不在于它写了多少行代码,而在于它用242个文件构建了一套城市服务小程序的“最小可行认知模型”。它告诉你,一个真实的本地生活产品,数据该怎么分层、界面该怎么组装、动效该怎么呼吸、模块该怎么握手。我把它用在三个真实项目里:给杭州文旅局做的“西湖漫步”,给成都餐饮协会做的“巷子美食”,给西安高校联盟做的“古城青年社交”。每一次,我都从index.wxml开始删减,留下核心骨架,再一点点植入真实数据和服务逻辑。它不是终点,而是你理解城市服务产品底层逻辑的起点——当你能看懂为什么135个GIF要放在/assets/gif/而不是/images/,为什么17个JSON要按业务域拆分,你就已经跨过了从“写代码”到“做产品”的那道门槛。
本文还有配套的精品资源,点击获取
简介:一套可直接运行的微信小程序城市生活服务源码,基于原生小程序框架开发,无需额外依赖,导入开发者工具即可预览调试。包含本地风景打卡、特色美食推荐、兴趣交友匹配、休闲娱乐活动、酒店住宿查询等核心功能模块,所有页面适配小程序规范。资源共242个文件:24个JS逻辑文件支撑业务流程,17个WXML构建页面结构,21个WXSS实现响应式样式,17个JSON管理配置与模拟数据,22个PNG图标和135个GIF动效增强交互体验。配套有readme.txt说明文档、LICENSE开源协议及.gitignore等工程配置文件,还集成wxParse富文本解析、百度地图bmap-wx.js、HTTP请求封装http.js等常用工具库。文件命名清晰,目录结构合理,适合用于城市类小程序快速原型开发、多模块协同学习、UI动效整合实践或二次定制上线。
本文还有配套的精品资源,点击获取
