├── .gitignore ├── README.md ├── client ├── app.js ├── app.json ├── app.wxss ├── components │ └── SectionTitle │ │ ├── SectionTitle.js │ │ ├── SectionTitle.json │ │ ├── SectionTitle.wxml │ │ └── SectionTitle.wxss ├── config.js ├── image │ ├── earth.png │ ├── home.png │ ├── leaf.png │ ├── selected-earth.png │ ├── selected-home.png │ ├── selected-leaf.png │ ├── selected-user.png │ └── user.png ├── pages │ ├── addCgi │ │ ├── addCgi.js │ │ ├── addCgi.json │ │ ├── addCgi.wxml │ │ ├── addCgi.wxss │ │ ├── code1.png │ │ └── code2.png │ ├── destination │ │ ├── destination.js │ │ ├── destination.json │ │ ├── destination.wxml │ │ └── destination.wxss │ ├── find │ │ ├── find.js │ │ ├── find.json │ │ ├── find.wxml │ │ └── find.wxss │ ├── index │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.wxss │ ├── me │ │ ├── me.js │ │ ├── me.json │ │ ├── me.wxml │ │ └── me.wxss │ └── search │ │ ├── search.js │ │ ├── search.json │ │ ├── search.wxml │ │ └── search.wxss ├── utils │ └── util.js └── vendor │ └── wafer2-client-sdk │ ├── LICENSE │ ├── README.md │ ├── index.js │ ├── lib │ ├── constants.js │ ├── login.js │ ├── request.js │ ├── session.js │ ├── tunnel.js │ ├── utils.js │ └── wxTunnel.js │ └── package.json ├── project.config.json ├── screen ├── find.gif ├── index.gif ├── me.gif ├── place.gif └── search.gif └── server ├── .eslintrc.json ├── README.md ├── app.js ├── config.js ├── controllers ├── index.js ├── login.js ├── message.js ├── travel.js ├── tunnel.js ├── upload.js └── user.js ├── middlewares └── response.js ├── nodemon.json ├── package-lock.json ├── package.json ├── process.prod.json ├── qcloud.js ├── routes └── index.js ├── tools.md └── tools ├── cAuth.sql └── initdb.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | *.pid.lock 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | # Coverage directory used by tools like istanbul 13 | coverage 14 | # nyc test coverage 15 | .nyc_output 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | # Bower dependency directory (https://bower.io/) 19 | bower_components 20 | # node-waf configuration 21 | .lock-wscript 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | # Dependency directories 25 | node_modules/ 26 | jspm_packages/ 27 | # Typescript v1 declaration files 28 | typings/ 29 | # Optional npm cache directory 30 | .npm 31 | # Optional eslint cache 32 | .eslintcache 33 | # Optional REPL history 34 | .node_repl_history 35 | # Output of 'npm pack' 36 | *.tgz 37 | # Yarn Integrity file 38 | .yarn-integrity 39 | # dotenv environment variables file 40 | .env 41 | .vscode 42 | # ignore sh 43 | sh/ 44 | # ignore test sdk.config.json 45 | sdk.config.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 旅游类微信小程序demo 2 | 3 | 一个旅游类型的小程序,主要用于熟悉小程序Api 4 | 5 | ## 项目截图 6 | 7 | 首页 8 | ![](screen/index.gif) 9 | 10 | 发现页 11 | ![](screen/find.gif) 12 | 13 | 地点页 14 | ![](screen/place.gif) 15 | 16 | 个人页 17 | ![](screen/me.gif) 18 | 19 | 搜索页 20 | ![](screen/search.gif) 21 | -------------------------------------------------------------------------------- /client/app.js: -------------------------------------------------------------------------------- 1 | //app.js 2 | var qcloud = require('./vendor/wafer2-client-sdk/index') 3 | var config = require('./config') 4 | 5 | App({ 6 | onLaunch: function () { 7 | qcloud.setLoginUrl(config.service.loginUrl) 8 | }, 9 | onLaunch: function (options) { 10 | // Do something initial when launch. 11 | }, 12 | onShow: function (options) { 13 | // Do something when show. 14 | }, 15 | onHide: function () { 16 | // Do something when hide. 17 | }, 18 | onError: function (msg) { 19 | console.log(msg) 20 | }, 21 | globalData: 'I am global data', 22 | }) 23 | /* 24 | 在其它页面的js里获取全局APP对象: 25 | var appInstance = getApp() 26 | console.log(appInstance.globalData) // I am global data 27 | 不要在定义于 App() 内的函数中调用 getApp() ,使用 this 就可以拿到 app 实例。 28 | 不要在 onLaunch 的时候调用 getCurrentPages(),此时 page 还没有生成。 29 | 通过 getApp() 获取实例之后,不要私自调用生命周期函数。 30 | */ 31 | 32 | /* 33 | onLaunch, onShow 参数: 34 | path String 打开小程序的路径 35 | query Object 打开小程序的query 36 | scene Number 打开小程序的场景值 37 | shareTicket String shareTicket,详见 获取更多转发信息 38 | referrerInfo Object 当场景为由从另一个小程序或公众号或App打开时,返回此字段 39 | referrerInfo.appId String 来源小程序或公众号或App的 appId,详见下方说明 40 | referrerInfo.extraData Object 来源小程序传过来的数据,scene=1037或1038时支持 41 | */ -------------------------------------------------------------------------------- /client/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "pages/index/index", 4 | "pages/me/me", 5 | "pages/addCgi/addCgi", 6 | "pages/destination/destination", 7 | "pages/find/find", 8 | "pages/search/search" 9 | ], 10 | "window": { 11 | "backgroundColor": "#F6F6F6", 12 | "backgroundTextStyle": "light", 13 | "navigationBarBackgroundColor": "#5fca9c", 14 | "navigationBarTitleText": "旅行呱", 15 | "navigationBarTextStyle": "white", 16 | "navigationStyle": "default", 17 | "enablePullDownRefresh": true, 18 | "onReachBottomDistance": 50 19 | }, 20 | "tabBar": { 21 | "color": "#d7d7d8", 22 | "selectedColor": "#5fca9c", 23 | "backgroundColor": "white", 24 | "borderStyle": "white", 25 | "position": "bottom", 26 | "list": [ 27 | { 28 | "pagePath": "pages/index/index", 29 | "text": "首页", 30 | "iconPath": "image/home.png", 31 | "selectedIconPath": "image/selected-home.png" 32 | }, 33 | { 34 | "pagePath": "pages/find/find", 35 | "text": "发现", 36 | "iconPath": "image/leaf.png", 37 | "selectedIconPath": "image/selected-leaf.png" 38 | }, 39 | { 40 | "pagePath": "pages/destination/destination", 41 | "text": "目的地", 42 | "iconPath": "image/earth.png", 43 | "selectedIconPath": "image/selected-earth.png" 44 | }, 45 | { 46 | "pagePath": "pages/me/me", 47 | "text": "我", 48 | "iconPath": "image/user.png", 49 | "selectedIconPath": "image/selected-user.png" 50 | } 51 | ] 52 | }, 53 | "networkTimeout": { 54 | "request": 60000, 55 | "connectSocket": 60000, 56 | "uploadFile": 60000, 57 | "downloadFile": 60000 58 | }, 59 | "debug": true 60 | } -------------------------------------------------------------------------------- /client/app.wxss: -------------------------------------------------------------------------------- 1 | /**app.wxss**/ 2 | page { 3 | background: #F6F6F6; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: flex-start; 7 | padding: 0; 8 | margin: 0; 9 | font-size: 28rpx; 10 | color:#686868; 11 | } 12 | 13 | view{ 14 | box-sizing: border-box; 15 | background-size: 100% 100%; 16 | margin: 0; 17 | padding: 0; 18 | } 19 | 20 | .container { 21 | display: flex; 22 | flex-direction: column; 23 | align-items: center; 24 | box-sizing: border-box; 25 | } 26 | 27 | .ellipsis{ 28 | overflow: hidden; 29 | text-overflow:ellipsis; 30 | white-space: nowrap; 31 | } 32 | .ellipsis-two{ 33 | overflow:hidden; 34 | text-overflow:ellipsis; 35 | display:-webkit-box; 36 | -webkit-box-orient:vertical; 37 | -webkit-line-clamp:2; 38 | } 39 | .ellipsis-three{ 40 | overflow:hidden; 41 | text-overflow:ellipsis; 42 | display:-webkit-box; 43 | -webkit-box-orient:vertical; 44 | -webkit-line-clamp:3; 45 | } 46 | .ellipsis-four{ 47 | overflow:hidden; 48 | text-overflow:ellipsis; 49 | display:-webkit-box; 50 | -webkit-box-orient:vertical; 51 | -webkit-line-clamp:4; 52 | } -------------------------------------------------------------------------------- /client/components/SectionTitle/SectionTitle.js: -------------------------------------------------------------------------------- 1 | // components/SectionTitle/SectionTitle.js 2 | Component({ 3 | options: { 4 | multipleSlots: false // 在组件定义时的选项中启用多slot支持 5 | }, 6 | /** 7 | * 组件的属性列表 8 | */ 9 | properties: { 10 | title: { 11 | type: String, 12 | value: '', 13 | }, 14 | color: { 15 | type: String, 16 | value: 'black', 17 | } 18 | }, 19 | 20 | /** 21 | * 组件的初始数据 22 | */ 23 | data: { 24 | 25 | }, 26 | 27 | /** 28 | * 组件的方法列表 29 | */ 30 | methods: { 31 | 32 | } 33 | }) 34 | -------------------------------------------------------------------------------- /client/components/SectionTitle/SectionTitle.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /client/components/SectionTitle/SectionTitle.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{title}} 4 | 更多 > 5 | 6 | -------------------------------------------------------------------------------- /client/components/SectionTitle/SectionTitle.wxss: -------------------------------------------------------------------------------- 1 | /* components/SectionTitle/SectionTitle.wxss */ 2 | .SectionTitle{ 3 | width: 100%; 4 | height: 100rpx; 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | padding: 0 20rpx; 9 | background-color: white; 10 | } 11 | .SectionTitle view{ 12 | font-weight: bold; 13 | font-size: 34rpx; 14 | } 15 | .SectionTitle text{ 16 | color:#1e1e1e; 17 | } -------------------------------------------------------------------------------- /client/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 小程序配置文件 3 | */ 4 | 5 | // 此处主机域名修改成腾讯云解决方案分配的域名 6 | var host = 'https://uii2lkfa.qcloud.la'; 7 | 8 | var config = { 9 | 10 | // 下面的地址配合云端 Demo 工作 11 | service: { 12 | host, 13 | 14 | // 登录地址,用于建立会话 15 | loginUrl: `${host}/weapp/login`, 16 | 17 | // 测试的请求地址,用于测试会话 18 | requestUrl: `${host}/weapp/user`, 19 | 20 | // 测试的信道服务地址 21 | tunnelUrl: `${host}/weapp/tunnel`, 22 | 23 | // 上传图片接口 24 | uploadUrl: `${host}/weapp/upload` 25 | } 26 | }; 27 | 28 | module.exports = config; 29 | -------------------------------------------------------------------------------- /client/image/earth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmh1996/wx-APP/dbec5001b58621b638eba1cdc814ddc232ac9936/client/image/earth.png -------------------------------------------------------------------------------- /client/image/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmh1996/wx-APP/dbec5001b58621b638eba1cdc814ddc232ac9936/client/image/home.png -------------------------------------------------------------------------------- /client/image/leaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmh1996/wx-APP/dbec5001b58621b638eba1cdc814ddc232ac9936/client/image/leaf.png -------------------------------------------------------------------------------- /client/image/selected-earth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmh1996/wx-APP/dbec5001b58621b638eba1cdc814ddc232ac9936/client/image/selected-earth.png -------------------------------------------------------------------------------- /client/image/selected-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmh1996/wx-APP/dbec5001b58621b638eba1cdc814ddc232ac9936/client/image/selected-home.png -------------------------------------------------------------------------------- /client/image/selected-leaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmh1996/wx-APP/dbec5001b58621b638eba1cdc814ddc232ac9936/client/image/selected-leaf.png -------------------------------------------------------------------------------- /client/image/selected-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmh1996/wx-APP/dbec5001b58621b638eba1cdc814ddc232ac9936/client/image/selected-user.png -------------------------------------------------------------------------------- /client/image/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmh1996/wx-APP/dbec5001b58621b638eba1cdc814ddc232ac9936/client/image/user.png -------------------------------------------------------------------------------- /client/pages/addCgi/addCgi.js: -------------------------------------------------------------------------------- 1 | //index.js 2 | var qcloud = require('../../vendor/wafer2-client-sdk/index') 3 | var config = require('../../config') 4 | var util = require('../../utils/util.js') 5 | 6 | Page({ 7 | data: { 8 | requestResult: '', 9 | canIUseClipboard: wx.canIUse('setClipboardData') 10 | }, 11 | onLoad: function (options) { 12 | // Do some initialize when page load. 13 | }, 14 | onReady: function () { 15 | // Do something when page ready. 16 | }, 17 | onShow: function () { 18 | // Do something when page show. 19 | }, 20 | onHide: function () { 21 | // Do something when page hide. 22 | }, 23 | onUnload: function () { 24 | // Do something when page close. 25 | }, 26 | onPullDownRefresh: function () { 27 | // Do something when pull down. 28 | }, 29 | onReachBottom: function () { 30 | // Do something when page reach bottom. 31 | }, 32 | onShareAppMessage: function () { 33 | return { 34 | title: '自定义转发标题', 35 | path: '/pages/addCgi/addCgi' 36 | } 37 | /* 38 | 只有定义了此事件处理函数,右上角菜单才会显示“转发”按钮 39 | 用户点击转发按钮的时候会调用 40 | 此事件需要 return 一个 Object,用于自定义转发内容 41 | */ 42 | }, 43 | onPageScroll: function (options) { 44 | //options.scrollTop 45 | // Do something when page scroll 46 | }, 47 | onTabItemTap(item) { 48 | console.log(item.index) 49 | console.log(item.pagePath) 50 | console.log(item.text) 51 | }, 52 | // Event handler. 53 | viewTap: function () { 54 | this.setData({ 55 | text: 'Set some data for updating view.' 56 | }, function () { 57 | // this is setData callback 58 | }) 59 | }, 60 | 61 | testCgi: function () { 62 | util.showBusy('请求中...') 63 | var that = this 64 | qcloud.request({ 65 | url: `${config.service.host}/weapp/demo`, 66 | login: false, 67 | success (result) { 68 | util.showSuccess('请求成功完成') 69 | that.setData({ 70 | requestResult: JSON.stringify(result.data) 71 | }) 72 | }, 73 | fail (error) { 74 | util.showModel('请求失败', error); 75 | console.log('request fail', error); 76 | } 77 | }) 78 | }, 79 | 80 | copyCode: function (e) { 81 | var codeId = e.target.dataset.codeId 82 | wx.setClipboardData({ 83 | data: code[codeId - 1], 84 | success: function () { 85 | util.showSuccess('复制成功') 86 | } 87 | }) 88 | } 89 | }) 90 | 91 | /* 92 | this.route获取当前页面路径。 93 | 94 | this.setData()的 key 可以非常灵活,以数据路径的形式给出,如 array[2].message,a.b.c.d,并且不需要在 this.data 中预先定义。 95 | 96 | navigateTo, redirectTo 只能打开非 tabBar 页面。 97 | switchTab 只能打开 tabBar 页面。 98 | reLaunch 可以打开任意页面。 99 | */ 100 | 101 | 102 | var code = [ 103 | `router.get('/demo', controllers.demo)`, 104 | `module.exports = ctx => { 105 | ctx.state.data = { 106 | msg: 'Hello World' 107 | } 108 | }` 109 | ] 110 | -------------------------------------------------------------------------------- /client/pages/addCgi/addCgi.json: -------------------------------------------------------------------------------- 1 | { 2 | "backgroundColor": "#F6F6F6", 3 | "backgroundTextStyle": "light", 4 | "navigationBarBackgroundColor": "#5fca9c", 5 | "navigationBarTitleText": "快速添加 CGI", 6 | "navigationBarTextStyle": "white", 7 | "navigationStyle": "default", 8 | "enablePullDownRefresh": true, 9 | "onReachBottomDistance": 50, 10 | "disableScroll": false 11 | } -------------------------------------------------------------------------------- /client/pages/addCgi/addCgi.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 测试 CGI 7 | 8 | 9 | 期望输出:{"code":0,"data":{"msg":"Hello World"}} 10 | 11 | 12 | {{'请求结果:' + requestResult}} 13 | 14 | 15 | 16 | 17 | 18 | 快速添加CGI指引 19 | 1. 打开 server/routes/index.js 文件,添加如下语句: 20 | 21 | 22 | 2. 在 server/controllers 下新建一个 demo.js 文件,写入如下代码: 23 | 24 | 25 | 3. 点击开发者工具右上角“腾讯云” - “上传测试代码”,勾选“智能上传” 26 | 4. 点击测试 CGI 按钮,即可看到结果 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /client/pages/addCgi/addCgi.wxss: -------------------------------------------------------------------------------- 1 | /**index.wxss**/ 2 | page { 3 | background: #F6F6F6; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: flex-start; 7 | } 8 | 9 | .list { 10 | margin-top: 40rpx; 11 | height: auto; 12 | width: 100%; 13 | background: #FFF; 14 | padding: 0 0 0 40rpx; 15 | border: 1px solid rgba(0, 0, 0, .1); 16 | border-left: none; 17 | border-right: none; 18 | transition: all 300ms ease; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: flex-start; 22 | box-sizing: border-box; 23 | } 24 | 25 | .list-item { 26 | width: 100%; 27 | padding: 0; 28 | line-height: 104rpx; 29 | font-size: 34rpx; 30 | color: #007AFF; 31 | border-top: 1px solid rgba(0, 0, 0, .1); 32 | display: flex; 33 | flex-direction: row; 34 | align-content: center; 35 | justify-content: space-between; 36 | box-sizing: border-box; 37 | } 38 | 39 | .list-item:first-child { 40 | border-top: none; 41 | } 42 | 43 | .request-text { 44 | color: #222; 45 | padding: 20rpx 0; 46 | font-size: 24rpx; 47 | line-height: 36rpx; 48 | word-break: break-all; 49 | } 50 | 51 | .guide { 52 | width: 100%; 53 | padding: 40rpx; 54 | box-sizing: border-box; 55 | display: flex; 56 | flex-direction: column; 57 | } 58 | 59 | .guide .headline { 60 | font-size: 34rpx; 61 | font-weight: bold; 62 | color: #555; 63 | line-height: 40rpx; 64 | } 65 | 66 | .guide .p { 67 | margin-top: 20rpx; 68 | font-size: 28rpx; 69 | line-height: 36rpx; 70 | color: #666; 71 | } 72 | 73 | .guide .code { 74 | margin-top: 20rpx; 75 | background: rgba(0, 0, 0, .8); 76 | padding: 20rpx; 77 | font-size: 28rpx; 78 | line-height: 36rpx; 79 | border-radius: 6rpx; 80 | color: #FFF; 81 | } 82 | 83 | .guide .image1 { 84 | margin-top: 20rpx; 85 | max-width: 100%; 86 | width: 356px; 87 | height: 47px; 88 | } 89 | 90 | .guide .image2 { 91 | margin-top: 20rpx; 92 | width: 264px; 93 | height: 100px; 94 | } 95 | 96 | .guide .copyBtn { 97 | width: 180rpx; 98 | font-size: 20rpx; 99 | margin-top: 16rpx; 100 | margin-left: 0; 101 | } 102 | -------------------------------------------------------------------------------- /client/pages/addCgi/code1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmh1996/wx-APP/dbec5001b58621b638eba1cdc814ddc232ac9936/client/pages/addCgi/code1.png -------------------------------------------------------------------------------- /client/pages/addCgi/code2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmh1996/wx-APP/dbec5001b58621b638eba1cdc814ddc232ac9936/client/pages/addCgi/code2.png -------------------------------------------------------------------------------- /client/pages/destination/destination.js: -------------------------------------------------------------------------------- 1 | // pages/destination/destination.js 2 | Page({ 3 | 4 | /** 5 | * 页面的初始数据 6 | */ 7 | data: { 8 | hotType:['景点','美食','酒店','购物','娱乐'], 9 | curHotTypeIndex:0, 10 | hotPlace: [{ 11 | img: 'https://img.alicdn.com/bao/uploaded/i1/170040265302404422/TB2RgTkuXXXXXbcXpXXXXXXXXXX_!!0-travel.jpg_400x400q75', 12 | name: '深圳世界之窗' 13 | }, { 14 | img: 'https://img.alicdn.com/bao/uploaded/i1/152410262911488745/TB2KOcptVXXXXauXXXXXXXXXXXX_!!0-travel.jpg_400x400q75', 15 | name: '深圳欢乐谷' 16 | }, { 17 | img: 'https://img.alicdn.com/bao/uploaded/i3/170040263573147307/TB2nY7KtVXXXXc1XpXXXXXXXXXX_!!0-travel.jpg_400x400q75', 18 | name: '东部华侨城' 19 | }, { 20 | img: 'https://img.alicdn.com/bao/uploaded/i1/170070262470115401/TB29XiDtVXXXXcKXpXXXXXXXXXX_!!0-travel.jpg_400x400q75', 21 | name: '锦绣中华' 22 | }, { 23 | img: 'https://img.alicdn.com/bao/uploaded/i3/170060268517979069/TB2383KXxfxQeBjSspjXXX4opXa_!!0-travel.jpg_400x400q75', 24 | name: '欢乐海岸' 25 | }], 26 | ranking:[ 27 | { 28 | img:'https://img.alicdn.com/i6/TB1JSQfgWagSKJjy0FgM3kRqFXa_112001.jpg_200x200q75', 29 | title:'醉美东西冲' 30 | }, { 31 | img: 'https://img.alicdn.com/i7/TB1t0NAXAT85uJjSZFhf.QPEVXa_033858.jpg_200x200q75', 32 | title: '地王大厦' 33 | }, { 34 | img: 'https://img.alicdn.com/i3/TB1imNnoCYH8KJjSspdBB7RgVXa_025044.jpg_200x200q75', 35 | title: '深港环岛游' 36 | } 37 | ], 38 | typical:[{ 39 | img:'https://img.alicdn.com/i6/TB1XoVBdlDH8KJjy1zet.djepXa_041525.jpg_200x200q75', 40 | title:'广州出发-深圳醉美盐田海滨栈道徒步大梅沙、小梅沙休闲摄影', 41 | num:'33%', 42 | }, { 43 | img: 'https://img.alicdn.com/i6/TB1J_BjjzihSKJjy0FeBE2JtpXa_115428.jpg_200x200q75', 44 | title: '广州深圳连线环香港岛双飞纯玩4天3晚跟团旅游珠江三角洲半自由行', 45 | num: '23%', 46 | }], 47 | otherCity:[{ 48 | img:'https://gw.alicdn.com/tps/TB1pCfxPXXXXXXPaFXXXXXXXXXX-800-800.jpg_100x100xzq75.jpg', 49 | name:'广州', 50 | km:'xx' 51 | }, { 52 | img: 'https://gw.alicdn.com/tps/TB10olrOVXXXXcOapXXXXXXXXXX-800-800.jpg_100x100xzq75.jpg', 53 | name: '佛山', 54 | km: 'xx' 55 | }, { 56 | img: 'https://gw.alicdn.com/tps/TB11Qt4OVXXXXakXpXXXXXXXXXX-800-800.jpg_100x100xzq75.jpg', 57 | name: '东莞', 58 | km: 'xx' 59 | }], 60 | route:[{ 61 | img:'https://img.alicdn.com/i8/TB1C_SSg_nI8KJjSszg9jk8ApXa_052920.jpg_200x200q75', 62 | title:'新八景之首-松湖烟雨骑行+百花洲百花齐放纯玩一日游', 63 | num:'235', 64 | price:'¥532' 65 | }, { 66 | img: 'https://img.alicdn.com/i3/TB1m1eZQVXXXXcYXpXXjYiB8FXX_025941.jpg_200x200q75', 67 | title: '春节深圳东部华侨城 咖酷旅馆2-6人套餐 送大侠谷茶溪谷双谷门票', 68 | num: '23', 69 | price: '¥518' 70 | }, { 71 | img: 'https://img.alicdn.com/i6/TB1gH8BmsrI8KJjy0FhzgDfnpXa_070043.jpg_200x200q75', 72 | title: '拖把儿 深圳模拟飞行驾驶舱全套VR体验机波音737飞机模拟舱训练机', 73 | num: '31', 74 | price: '¥398' 75 | }] 76 | }, 77 | 78 | selectHotType(e){ 79 | this.setData({ 80 | curHotTypeIndex: e.target.dataset.index 81 | }) 82 | }, 83 | 84 | 85 | /** 86 | * 生命周期函数--监听页面加载 87 | */ 88 | onLoad: function (options) { 89 | 90 | }, 91 | 92 | /** 93 | * 生命周期函数--监听页面初次渲染完成 94 | */ 95 | onReady: function () { 96 | 97 | }, 98 | 99 | /** 100 | * 生命周期函数--监听页面显示 101 | */ 102 | onShow: function () { 103 | 104 | }, 105 | 106 | /** 107 | * 生命周期函数--监听页面隐藏 108 | */ 109 | onHide: function () { 110 | 111 | }, 112 | 113 | /** 114 | * 生命周期函数--监听页面卸载 115 | */ 116 | onUnload: function () { 117 | 118 | }, 119 | 120 | /** 121 | * 页面相关事件处理函数--监听用户下拉动作 122 | */ 123 | onPullDownRefresh: function () { 124 | 125 | }, 126 | 127 | /** 128 | * 页面上拉触底事件的处理函数 129 | */ 130 | onReachBottom: function () { 131 | 132 | }, 133 | 134 | /** 135 | * 用户点击右上角分享 136 | */ 137 | onShareAppMessage: function () { 138 | 139 | } 140 | }) -------------------------------------------------------------------------------- /client/pages/destination/destination.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "目的地", 3 | "usingComponents": { 4 | "section-title": "../../components/SectionTitle/SectionTitle" 5 | } 6 | } -------------------------------------------------------------------------------- /client/pages/destination/destination.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 深圳 10 | 广东人气第2名 11 | 6 ~ 12℃ 12 | 13 | 14 | 精选图库 > 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 最近浏览 25 | 上海 26 | 27 | 28 | 29 | 30 | 31 | 32 | 玩法 33 | 深圳经典线路三日游 34 | 35 | 36 | 37 | 38 | 39 | 40 | 榜单 41 | 排在前面的景点你都去过了吗 42 | 43 | 44 | 45 | 46 | 47 | 游记 48 | 看看达人怎么玩 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {{item}} 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 人气第{{(index+1)}}名 67 | 68 | {{item.name}} 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | {{item.title}} 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | {{item.title}} 95 | 96 | {{item.num}} 97 | 到访的游客选择这条线路 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | {{item.title}} 111 | 112 | 最近有{{item.num}}人已经购买 113 | {{item.price}} 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | {{item.name}} 126 | 127 | 相距 128 | {{item.km}} 129 | 公里 130 | 131 | 132 | 133 | 134 | 135 | 136 | 想和朋友们分享这个目的地吗?立即分享 137 | 138 | 139 | -------------------------------------------------------------------------------- /client/pages/destination/destination.wxss: -------------------------------------------------------------------------------- 1 | /* pages/destination/destination.wxss */ 2 | .header{ 3 | width: 100%; 4 | height: 360rpx; 5 | } 6 | .header .swiperItem{ 7 | position: relative; 8 | } 9 | .header .swiperItem image{ 10 | position: absolute; 11 | width: 100%; 12 | height: 100%; 13 | top: 0; 14 | left: 0; 15 | } 16 | .header .swiperItem .placeInfo{ 17 | position: absolute; 18 | width: 100%; 19 | height: 100%; 20 | top: 0; 21 | left: 0; 22 | padding-left: 30rpx; 23 | padding-bottom: 50rpx; 24 | display: flex; 25 | justify-content: space-between; 26 | align-items: flex-end; 27 | color: white; 28 | } 29 | .header .swiperItem .placeInfo .left{ 30 | display: flex; 31 | flex-direction: column; 32 | justify-content: flex-end; 33 | } 34 | .header .swiperItem .placeInfo .left .name{ 35 | font-size: 54rpx; 36 | font-weight: bold; 37 | margin-bottom: 20rpx; 38 | } 39 | .header .swiperItem .placeInfo .left .rank{ 40 | background-color: #f8435e; 41 | padding: 4rpx; 42 | padding-left: 10rpx; 43 | padding-right: 14rpx; 44 | border-bottom-right-radius: 20rpx; 45 | margin-bottom: 20rpx; 46 | } 47 | .header .swiperItem .placeInfo .right view{ 48 | background-color: rgba(0, 0, 0, 0.4); 49 | padding: 8rpx; 50 | padding-left: 12rpx; 51 | border-top-left-radius: 30rpx; 52 | border-bottom-left-radius: 30rpx; 53 | } 54 | .header .normalItem image{ 55 | width: 100%; 56 | height: 100%; 57 | } 58 | 59 | .content{ 60 | background-color: white; 61 | padding: 20rpx; 62 | display: flex; 63 | flex-direction: column; 64 | margin-bottom: 20rpx; 65 | width: 100%; 66 | } 67 | .section{ 68 | background-color: white; 69 | display: flex; 70 | flex-direction: column; 71 | margin-bottom: 20rpx; 72 | width: 100%; 73 | } 74 | 75 | .recently{ 76 | flex-direction: row; 77 | align-items: center; 78 | height: 80rpx; 79 | margin-bottom: 0; 80 | } 81 | .recently view{ 82 | margin-left: 15rpx; 83 | color: #1e1e1e; 84 | width: 200rpx; 85 | height: 50rpx; 86 | line-height: 50rpx; 87 | text-align: center; 88 | background-color: #f0f2f1; 89 | border-radius: 30rpx; 90 | } 91 | 92 | .mainSection .item{ 93 | position: relative; 94 | color:white; 95 | } 96 | .mainSection .item image{ 97 | position: absolute; 98 | top: 0; 99 | left: 0; 100 | width: 100%; 101 | height: 100%; 102 | } 103 | .mainSection .item .tips{ 104 | background-color: rgba(0, 0, 0, 0.4); 105 | position: absolute; 106 | top: 0; 107 | left: 0; 108 | padding: 20rpx; 109 | width: 100%; 110 | height: 100%; 111 | display: flex; 112 | flex-direction: column; 113 | justify-content: space-between; 114 | } 115 | .mainSection .item .tips text:first-child{ 116 | font-size: 40rpx; 117 | font-weight: bold; 118 | } 119 | .mainSection .item .tips text:last-child{ 120 | font-size: 30rpx; 121 | } 122 | .mainSection .top{ 123 | width: 100%; 124 | height: 200rpx; 125 | margin-bottom: 7rpx; 126 | } 127 | .mainSection .bottom{ 128 | display: flex; 129 | justify-content: space-between; 130 | width: 100%; 131 | height: 200rpx; 132 | } 133 | .mainSection .bottom .left{ 134 | width: 49.5%; 135 | height: 100%; 136 | } 137 | .mainSection .bottom .right{ 138 | width: 49.5%; 139 | height: 100%; 140 | } 141 | 142 | .hot .typeList{ 143 | padding: 0 20rpx; 144 | padding-bottom: 20rpx; 145 | width: 100%; 146 | display: flex; 147 | justify-content: space-between; 148 | } 149 | .hot .typeList view{ 150 | width: 130rpx; 151 | height: 70rpx; 152 | text-align: center; 153 | line-height: 70rpx; 154 | font-size: 30rpx; 155 | color:black; 156 | border: 1px solid #e6e6e6; 157 | border-radius: 8rpx; 158 | } 159 | .hot .typeList .selected{ 160 | color:white; 161 | background-color: #ee9900; 162 | } 163 | 164 | .scrollBox{ 165 | overflow:hidden; 166 | white-space:nowrap; 167 | } 168 | .hot .scrollList{ 169 | width: 100%; 170 | height: 300rpx; 171 | } 172 | .hot .scrollList .scrollItem{ 173 | width: 220rpx; 174 | height: 100%; 175 | display: inline-block; 176 | margin-left: 20rpx; 177 | } 178 | .hot .scrollList .scrollItem .imgBox{ 179 | width: 100%; 180 | height: 220rpx; 181 | position: relative; 182 | } 183 | .hot .scrollList .scrollItem .imgBox image{ 184 | width: 100%; 185 | height: 100%; 186 | position: absolute; 187 | top: 0; 188 | left: 0; 189 | } 190 | .hot .scrollList .scrollItem .imgBox view{ 191 | padding: 4rpx; 192 | color: white; 193 | font-size: 20rpx; 194 | background-color: #f8435e; 195 | border-bottom-right-radius: 20rpx; 196 | position: absolute; 197 | top: 0; 198 | left: 0; 199 | } 200 | .hot .scrollList .scrollItem .name{ 201 | width: 100%; 202 | color: black; 203 | font-size: 30rpx; 204 | margin-top: 10rpx; 205 | } 206 | 207 | .rankList .scrollList{ 208 | width: 100%; 209 | height: 200rpx; 210 | margin-bottom: 20rpx; 211 | } 212 | .rankList .scrollList .scrollItem{ 213 | width: 300rpx; 214 | height: 100%; 215 | display: inline-block; 216 | margin-left: 20rpx; 217 | position: relative; 218 | } 219 | .rankList .scrollList .scrollItem image{ 220 | position: absolute; 221 | top: 0; 222 | left: 0; 223 | width: 100%; 224 | height: 100%; 225 | } 226 | .rankList .scrollList .scrollItem .wrapper{ 227 | position: absolute; 228 | top: 0; 229 | left: 0; 230 | width: 100%; 231 | height: 100%; 232 | background-color: rgba(0, 0, 0, 0.4); 233 | display: flex; 234 | justify-content: center; 235 | align-items: center; 236 | } 237 | .rankList .scrollList .scrollItem .wrapper view{ 238 | width: 260rpx; 239 | height: 60rpx; 240 | line-height: 60rpx; 241 | color: white; 242 | font-size: 36rpx; 243 | font-weight: bold; 244 | text-align: center; 245 | } 246 | 247 | .typical .scrollList{ 248 | width: 100%; 249 | height: 460rpx; 250 | } 251 | .typical .scrollList .scrollItem{ 252 | width: 600rpx; 253 | height: 100%; 254 | display: inline-block; 255 | margin-left: 20rpx; 256 | } 257 | .typical .scrollList .scrollItem image{ 258 | width: 100%; 259 | height: 300rpx; 260 | display: block; 261 | margin-bottom: 20rpx; 262 | } 263 | .typical .scrollList .scrollItem .title{ 264 | width: 100%; 265 | height: 60rpx; 266 | line-height: 60rpx; 267 | display: block; 268 | color: black; 269 | font-size: 34rpx; 270 | } 271 | .typical .scrollList .scrollItem .tips{ 272 | width: 100%; 273 | height: 60rpx; 274 | display: flex; 275 | align-items: center; 276 | } 277 | .typical .scrollList .scrollItem .tips view{ 278 | width: 60rpx; 279 | height: 40rpx; 280 | color: white; 281 | background-color: #ec9b00; 282 | text-align: center; 283 | line-height: 40rpx; 284 | } 285 | .typical .scrollList .scrollItem .tips text{ 286 | margin-left: 10rpx; 287 | } 288 | 289 | .routeList{ 290 | width: 100%; 291 | display: flex; 292 | flex-direction: column; 293 | padding: 0 20rpx 20rpx; 294 | } 295 | .routeList .routeItem{ 296 | width: 100%; 297 | height: 200rpx; 298 | display: flex; 299 | justify-content: space-between; 300 | margin: 20rpx 0; 301 | border-bottom: 2rpx solid #f6f6f6; 302 | } 303 | .routeList .routeItem .left{ 304 | width: 230rpx; 305 | height: 100%; 306 | } 307 | .routeList .routeItem .right{ 308 | width: 450rpx; 309 | display: flex; 310 | flex-direction: column; 311 | justify-content: space-between; 312 | height: 100%; 313 | } 314 | .routeList .routeItem .right .title{ 315 | color: black; 316 | font-size: 32rpx; 317 | } 318 | .routeList .routeItem .right .tips{ 319 | display: flex; 320 | justify-content: space-between; 321 | width: 100%; 322 | height: 40rpx; 323 | align-items: flex-end; 324 | } 325 | .routeList .routeItem .right .tips .tips1{ 326 | 327 | } 328 | .routeList .routeItem .right .tips .tips1 text{ 329 | color: orange; 330 | } 331 | .routeList .routeItem .right .tips .tips2{ 332 | 333 | } 334 | .routeList .routeItem .right .tips .tips2 text{ 335 | color: orangered; 336 | font-size: 34rpx; 337 | } 338 | 339 | 340 | .otherCity .otherCityList{ 341 | width: 100%; 342 | padding: 20rpx; 343 | display: flex; 344 | justify-content: space-between; 345 | } 346 | .otherCity .otherCityList .city{ 347 | width: 225rpx; 348 | height: 330rpx; 349 | display: flex; 350 | flex-direction: column; 351 | justify-content: space-between; 352 | } 353 | .otherCity .otherCityList .city image{ 354 | width: 100%; 355 | height: 220rpx; 356 | } 357 | .otherCity .otherCityList .city .name{ 358 | color: black; 359 | font-size: 32rpx; 360 | } 361 | .otherCity .otherCityList .city .away .km{ 362 | color: #ee9f08; 363 | } 364 | .bottomTips{ 365 | width: 100%; 366 | height: 100rpx; 367 | text-align: center; 368 | line-height: 90rpx; 369 | font-size: 28rpx; 370 | color: #747675; 371 | } -------------------------------------------------------------------------------- /client/pages/find/find.js: -------------------------------------------------------------------------------- 1 | // pages/find/find.js 2 | Page({ 3 | 4 | /** 5 | * 页面的初始数据 6 | */ 7 | data: { 8 | findBtn:[ 9 | { 10 | icon:'https://gw.alicdn.com/tfs/TB1XhMFbwoQMeJjy0FoXXcShVXa-48-48.png_48x48q50.jpg_.webp', 11 | text:'笔记' 12 | }, { 13 | icon: 'https://gw.alicdn.com/tfs/TB13HEsbrsTMeJjSszhXXcGCFXa-48-48.png_48x48q50.jpg_.webp', 14 | text: '头条' 15 | }, { 16 | icon: 'https://gw.alicdn.com/tfs/TB1AcoBbEgQMeJjy0FgXXc5dXXa-48-48.png_48x48q50.jpg_.webp', 17 | text: '游记' 18 | }, { 19 | icon: 'https://gw.alicdn.com/tfs/TB1_xIFbwoQMeJjy0FoXXcShVXa-48-48.png_48x48q50.jpg_.webp', 20 | text: '买家秀' 21 | } 22 | ], 23 | bannerList:[ 24 | { img:'https://gw.alicdn.com/tfs/TB16PvKf26H8KJjy0FjXXaXepXa-750-180.jpg_760x760q75.jpg_.webp' 25 | }, { 26 | img: 'https://gw.alicdn.com/tfs/TB15IBqoLDH8KJjy1XcXXcpdXXa-750-234.png_760x760q75.jpg_.webp' 27 | }, { 28 | img: 'https://gw.alicdn.com/tfs/TB1ROktnBTH8KJjy0FiXXcRsXXa-750-234.jpg_760x760q75.jpg_.webp' 29 | }, { 30 | img: 'https://gw.alicdn.com/tfs/TB1_C6upcLJ8KJjy0FnXXcFDpXa-750-234.jpg_760x760q75.jpg_.webp' 31 | }], 32 | feedList:[], 33 | loadTimes: 0, 34 | perLoad: 4, 35 | dataList:[ 36 | { 37 | userImg:'https://m.tuniucdn.com/filebroker/cdn/snc/e0/42/e0424052abef7fbcb73359c9279fb849_w120_h120_c1_t0_w120_h120_c1_t0.jpg', 38 | userName:'吴秋煌', 39 | content:'此篇游记是“吴秋煌”在途牛发表的游记攻略,记录了在“肇庆”旅游时的游记攻略,图片多多,真实好玩。', 40 | imgList: ['https://m.tuniucdn.com/fb2/t1/G5/M00/51/9E/Cii-s1pxQaSIFogLAAWHUQgwZU8AAC-RgG2fr4ABYdp67_w800_h0_c0_t0.jpeg', 'https://m.tuniucdn.com/fb2/t1/G5/M00/51/9E/Cii-s1pxQaSIFogLAAWHUQgwZU8AAC-RgG2fr4ABYdp67_w800_h0_c0_t0.jpeg'], 41 | company:'游在肇庆' 42 | }, { 43 | userImg: 'https://m.tuniucdn.com/fb2/t1/G5/M00/4E/5B/Cii-s1pujnaIBBAuABeZh8xcHEYAAC58wD1ASAAF5mf376_w120_h120_c1_t0_w120_h120_c1_t0.jpg', 44 | userName: 'vivian婷ting', 45 | content: '又要去澳大利亚了?这次我去的是西澳,有没有听到过珀斯这个地方?我也是第一次听到,原来我是要去毛里求斯的,可恶的航空公司取消了航线,还好我的签证还有效,说走就走。。。', 46 | imgList: ['https://m.tuniucdn.com/fb2/t1/G5/M00/50/18/Cii-s1pwAGOISR39AAH8Fz5XY8wAAC8VwHkjqoAAfwv431_w800_h0_c0_t0.jpg', 'https://m.tuniucdn.com/fb2/t1/G5/M00/50/18/Cii-s1pwAGOISR39AAH8Fz5XY8wAAC8VwHkjqoAAfwv431_w800_h0_c0_t0.jpg', 'https://m.tuniucdn.com/fb2/t1/G5/M00/50/18/Cii-s1pwAGOISR39AAH8Fz5XY8wAAC8VwHkjqoAAfwv431_w800_h0_c0_t0.jpg'], 47 | company: '西澳大利亚' 48 | }, { 49 | userImg: 'https://m.tuniucdn.com/fb2/t1/G5/M00/07/CA/Cii-slok4dCIRWQZAH0p2IGs8cIAAAYwQD2IkkAfSnw333_w120_h120_c1_t0_w120_h120_c1_t0.jpg', 50 | userName: '熊猫慢游', 51 | content: '经过了四个多小时车子到达了大叻,来时的山路也不是那么的颠簸,至少并没有给我留下特别深刻的印象,说明应该还是和平常的路没有多大的区别;怎么和在拉萨的时候一样啊,这是下车之后我的第一感觉,因为大叻属于高山小镇,海拔已经比海平面升高了许多', 52 | imgList: ['https://m.tuniucdn.com/fb2/t1/G5/M00/4F/81/Cii-tFpvOuuIABgqAAkuLHhQo3UAAC7fgJPD3kACS5E996_w800_h0_c0_t0.jpg', 'https://m.tuniucdn.com/fb2/t1/G5/M00/4F/81/Cii-tFpvOuuIABgqAAkuLHhQo3UAAC7fgJPD3kACS5E996_w800_h0_c0_t0.jpg', 'https://m.tuniucdn.com/fb2/t1/G5/M00/4F/81/Cii-tFpvOuuIABgqAAkuLHhQo3UAAC7fgJPD3kACS5E996_w800_h0_c0_t0.jpg'], 53 | company: '游在大叻' 54 | }, { 55 | userImg: 'https://m.tuniucdn.com/fb2/t1/G5/M00/07/CA/Cii-slok4dCIRWQZAH0p2IGs8cIAAAYwQD2IkkAfSnw333_w120_h120_c1_t0_w120_h120_c1_t0.jpg', 56 | userName: '熊猫慢游', 57 | content: '昨天下午上车之后并也没有睡觉,而是用手机在找寻着芽庄的旅店,之后便是一觉睡到车子到达芽庄,唯一中间有点印象的就是记得半夜有被车子颠醒过一次,然后迷迷糊糊的看见窗外星星点点的灯光一字排开非常漂亮,应该是正沿着海岸线在行驶,但是实在是太困了,根本没有继续观看便又熟睡过去了,可能也正在因为这样的匆匆一瞥让我久久的在回味那美景。', 58 | imgList: ['https://m.tuniucdn.com/fb2/t1/G5/M00/4F/51/Cii-s1pvGmGIePqwAA7opuM88v4AAC7YAE_nJ0ADui-288_w800_h0_c0_t0.jpg', 'https://m.tuniucdn.com/fb2/t1/G5/M00/4F/51/Cii-s1pvGmGIePqwAA7opuM88v4AAC7YAE_nJ0ADui-288_w800_h0_c0_t0.jpg'], 59 | company: '游在芽庄' 60 | }, { 61 | userImg: 'https://images.tuniucdn.com/head/2/2143812s.jpg', 62 | userName: '悦微小鱼', 63 | content: '宝钢是1978年12月23日开工建设的,我们四人都是1980年进宝钢工作的,阿根、阿贵是退伍军人,阿芳和我是2000名高中生,而阿根、阿贵分别将于2018年8月、10月光荣退休,想想在一起工作和生活了38年,转眼从弱冠之年到了花甲之年,不久之后将要分别,真是有点依依不舍和两眼泪汪汪的感觉,希望友情永驻。', 64 | imgList: ['https://m.tuniucdn.com/fb2/t1/G5/M00/4F/64/Cii-tFpvKLeIEwYqAAX2fJaKOs4AAC7bQHXq-YABfaU652_w800_h0_c0_t0.JPG'], 65 | company: '游在三亚' 66 | }, { 67 | userImg: 'https://m.tuniucdn.com/fb2/t1/G3/M00/1C/A0/Cii_LllTdRqIXRl4AAWySHDrT3gAAB35wIk7v8ABbJg999_w120_h120_c1_t0_w120_h120_c1_t0.jpg', 68 | userName: '雪泡儿', 69 | content: '成都,天府之城,是我最喜欢的城市,满眼的火锅,街道上搓麻的人们都是我爱成都的原因,你可以只为吃,也可以去看一次蜀风雅韵的表演,喝一碗茶,逛一逛有童年记忆的公园,都无比悠闲。', 70 | imgList: ['https://m.tuniucdn.com/fb2/t1/G5/M00/4E/EE/Cii-slpu3p-IWMMzAALdpExCNqYAAC64ACyYEIAAt2844_w800_h0_c0_t0.jpeg', 'https://m.tuniucdn.com/fb2/t1/G5/M00/4E/EE/Cii-slpu3p-IWMMzAALdpExCNqYAAC64ACyYEIAAt2844_w800_h0_c0_t0.jpeg'], 71 | company: '游在宽窄巷子' 72 | }, { 73 | userImg: 'https://m.tuniucdn.com/fb2/t1/G2/M00/2B/CC/Cii-T1g7iJmILXDfAAl7gR5XZFcAAE6cABV-J8ACXuZ398_w120_h120_c1_t0_w120_h120_c1_t0.jpg', 74 | userName: '黏黏小狐狸', 75 | content: '南京是一个计划了好久的地方,这次趁着途牛有活动买的机加酒的套餐其实非常划算。', 76 | imgList: ['https://m.tuniucdn.com/fb2/t1/G5/M00/4D/94/Cii-tFptdNaIeSjyAAOY5XFzJpEAAC5EABv-ucAA5j9262_w800_h0_c0_t0.JPG', 'https://m.tuniucdn.com/fb2/t1/G5/M00/4D/94/Cii-tFptdNaIeSjyAAOY5XFzJpEAAC5EABv-ucAA5j9262_w800_h0_c0_t0.JPG', 'https://m.tuniucdn.com/fb2/t1/G5/M00/4D/94/Cii-tFptdNaIeSjyAAOY5XFzJpEAAC5EABv-ucAA5j9262_w800_h0_c0_t0.JPG'], 77 | company: '侵华日军南京大屠杀遇难同胞纪念馆' 78 | }, { 79 | userImg: 'https://m.tuniucdn.com/fb2/t1/G2/M00/9B/A5/Cii-TlkFPOiIVqjFAEYBV6mPBdQAAJhNwH7OOcARgFv182_w120_h120_c1_t0_w120_h120_c1_t0.jpg', 80 | userName: '夏草', 81 | content: '去年底、在途牛网发现越南一款十二日深度游的旅游产品、我就豪不犹豫的下单了、尽管这次去越南己是第三次了、我的第一次出国就是河内下龙湾、第二次去越南是胡志明市仅过了一夜。二次去越南己是八、九年之前的事了。这次虽然有重复、但美拖、头顿、芽庄、大勒、顺化、会安、岘港都是第一次去、事实证明、这次值得!', 82 | imgList: ['https://m.tuniucdn.com/fb2/t1/G5/M00/4D/A1/Cii-slptf92ITMo5AAlRB8IJE6UAAC5HwIxkYQACVEf829_w800_h0_c0_t0.JPG', 'https://m.tuniucdn.com/fb2/t1/G5/M00/4D/A1/Cii-slptf92ITMo5AAlRB8IJE6UAAC5HwIxkYQACVEf829_w800_h0_c0_t0.JPG'], 83 | company: '游在胡志明市' 84 | } 85 | ] 86 | }, 87 | 88 | loadMore: function () { 89 | const loadTimes = this.data.loadTimes; 90 | const perLoad = this.data.perLoad; 91 | let temArr = this.data.dataList.slice((loadTimes * perLoad), (loadTimes * perLoad) + perLoad); 92 | if (temArr.length <= 0) { 93 | return; 94 | } 95 | 96 | wx.showLoading({ 97 | title: '加载中', 98 | }); 99 | setTimeout(() => { 100 | let feedListArr = this.data.feedList.slice(); 101 | feedListArr.push(...temArr); 102 | this.setData({ 103 | feedList: feedListArr, 104 | loadTimes: loadTimes + 1 105 | }, () => { 106 | wx.hideLoading(); 107 | }) 108 | }, 1000); 109 | }, 110 | 111 | /** 112 | * 生命周期函数--监听页面加载 113 | */ 114 | onLoad: function (options) { 115 | 116 | }, 117 | 118 | /** 119 | * 生命周期函数--监听页面初次渲染完成 120 | */ 121 | onReady: function () { 122 | 123 | }, 124 | 125 | /** 126 | * 生命周期函数--监听页面显示 127 | */ 128 | onShow: function () { 129 | 130 | }, 131 | 132 | /** 133 | * 生命周期函数--监听页面隐藏 134 | */ 135 | onHide: function () { 136 | 137 | }, 138 | 139 | /** 140 | * 生命周期函数--监听页面卸载 141 | */ 142 | onUnload: function () { 143 | 144 | }, 145 | 146 | /** 147 | * 页面相关事件处理函数--监听用户下拉动作 148 | */ 149 | onPullDownRefresh: function () { 150 | 151 | }, 152 | 153 | /** 154 | * 页面上拉触底事件的处理函数 155 | */ 156 | onReachBottom: function () { 157 | this.loadMore(); 158 | }, 159 | 160 | /** 161 | * 用户点击右上角分享 162 | */ 163 | onShareAppMessage: function () { 164 | 165 | } 166 | }) -------------------------------------------------------------------------------- /client/pages/find/find.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "发现" 3 | } -------------------------------------------------------------------------------- /client/pages/find/find.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{item.text}} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 精选推荐 22 | 23 | 24 | 25 | 26 | 27 | 28 | 2018适合带父母旅行的好地方(内附游玩攻略)! 29 | 精选 30 | 31 | 我们陪伴父母的时间越来越少,不止我们渴望一次说走就走的旅行,父母也一样,抽点时间带他们出去玩玩吧 32 | 33 | 34 | 35 | 36 | 为什么这辈子一定要去一次南极为什么这辈子一定要去一次南极为什么这辈子一定要去一次南极 37 | 可订 38 | 39 | 40 | 41 | 南极有一种蓝,诗人和高级的摄影技术都拍不出来的纯净的蓝色,这辈子,一定要亲眼去看看南极有一种蓝,诗人和高级的摄影技术都拍不出来的纯净的蓝色,这辈子,一定要亲眼去看看南极有一种蓝,诗人和高级的摄影技术都拍不出来的纯净的蓝色,这辈子,一定要亲眼去看看南极有一种蓝,诗人和高级的摄影技术都拍不出来的纯净的蓝色,这辈子,一定要亲眼去看看 42 | 43 | 44 | 45 | 旅行值得买 46 | 47 | 阅读量 365 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {{item.userName}} 57 | 58 | {{item.content}} 59 | 60 | 61 | 62 | 63 | 买家秀 64 | 65 | {{item.company}} 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /client/pages/find/find.wxss: -------------------------------------------------------------------------------- 1 | /* pages/find/find.wxss */ 2 | .findTop{ 3 | width: 100%; 4 | height: 400rpx; 5 | position: relative; 6 | background-color: #3c87b6; 7 | } 8 | .findTop .bg{ 9 | position: absolute; 10 | top: 0; 11 | left: 0; 12 | width: 100%; 13 | height: 95%; 14 | background-image: url(https://gw.alicdn.com/imgextra/i2/6000000002937/TB2iTZQtSFjpuFjSspbXXXagVXa_!!0-travel.jpg_760x760q50.jpg_.webp); 15 | filter: blur(10rpx); 16 | z-index: 0; 17 | } 18 | .findTop .btnList{ 19 | width: 100%; 20 | display: flex; 21 | height: 180rpx; 22 | justify-content: space-around; 23 | align-items: center; 24 | position: absolute; 25 | top: 0; 26 | } 27 | .findTop .btnList .btnItem{ 28 | width: 140rpx; 29 | color: white; 30 | display: flex; 31 | flex-direction: column; 32 | align-items: center; 33 | } 34 | .findTop .btnList .btnItem image{ 35 | width: 60rpx; 36 | height: 60rpx; 37 | margin-bottom: 10rpx; 38 | } 39 | .findTop .btnList .btnItem text{ 40 | font-size: 30rpx; 41 | } 42 | .scrollBox{ 43 | overflow:hidden; 44 | white-space:nowrap; 45 | margin-top: 180rpx; 46 | } 47 | .findTop .scrollList{ 48 | width: 100%; 49 | height: 200rpx; 50 | } 51 | .findTop .scrollList .scrollItem{ 52 | width: 330rpx; 53 | height: 200rpx; 54 | position: relative; 55 | display: inline-block; 56 | margin-left: 10rpx; 57 | } 58 | .findTop .scrollList .scrollItem image{ 59 | position: absolute; 60 | top: 0; 61 | left: 0; 62 | width: 100%; 63 | height: 100%; 64 | border-radius: 40rpx; 65 | } 66 | 67 | .bar{ 68 | width: 100%; 69 | height: 100rpx; 70 | display: flex; 71 | align-items: center; 72 | padding-left: 30rpx; 73 | background: white; 74 | } 75 | .bar view{ 76 | font-size: 36rpx; 77 | color:#1e1e1e; 78 | font-weight: 600; 79 | padding-left: 30rpx; 80 | border-left: 4rpx solid #1e1e1e; 81 | height: 40rpx; 82 | flex: 1; 83 | } 84 | 85 | .feedList{ 86 | display: flex; 87 | flex-direction: column; 88 | } 89 | .feedList .content{ 90 | width: 100%; 91 | margin-top: 16rpx; 92 | background: white; 93 | padding: 20rpx; 94 | display: flex; 95 | flex-direction: column; 96 | } 97 | .feedList .topic{ 98 | 99 | } 100 | .feedList .topic image{ 101 | width: 100%; 102 | height: 300rpx; 103 | } 104 | .feedList .topic .title{ 105 | width: 100%; 106 | height: 80rpx; 107 | display: flex; 108 | justify-content: space-between; 109 | align-items: center; 110 | } 111 | .feedList .topic .title .text{ 112 | font-weight: 600; 113 | color: black; 114 | font-size: 38rpx; 115 | width: 600rpx; 116 | height: 50rpx; 117 | } 118 | .feedList .topic .title .type{ 119 | background-color: #f0f2f1; 120 | padding: 8rpx; 121 | } 122 | .feedList .topic .intro{ 123 | line-height: 1.7em; 124 | font-size: 30rpx; 125 | } 126 | .feedList .article .title{ 127 | width: 100%; 128 | height: 80rpx; 129 | display: flex; 130 | justify-content: space-between; 131 | } 132 | .feedList .article .title .text{ 133 | font-weight: 600; 134 | color: black; 135 | font-size: 38rpx; 136 | width: 600rpx; 137 | height: 50rpx; 138 | } 139 | .feedList .article .title .type{ 140 | background-color: #f54f00; 141 | padding: 8rpx 14rpx; 142 | color:white; 143 | height: 50rpx; 144 | position: relative; 145 | right: -20rpx; 146 | } 147 | .feedList .article .sub{ 148 | display: flex; 149 | width: 100%; 150 | height: 220rpx; 151 | justify-content: space-between; 152 | } 153 | .feedList .article .sub image{ 154 | width: 300rpx; 155 | height: 100%; 156 | margin-right: 30rpx; 157 | } 158 | .feedList .article .sub view{ 159 | flex:1; 160 | height: 100%; 161 | line-height: 1.7em; 162 | font-size: 32rpx; 163 | color: #676767; 164 | } 165 | .feedList .article .tips{ 166 | display: flex; 167 | justify-content: space-between; 168 | align-items: center; 169 | margin-top: 30rpx; 170 | } 171 | .feedList .article .tips .author{ 172 | background-color: #f0f2f1; 173 | padding: 8rpx; 174 | } 175 | .feedList .article .tips .data{ 176 | font-size: 26rpx; 177 | } 178 | .feedList .userFeed .userInfo{ 179 | width: 100%; 180 | height: 60rpx; 181 | display: flex; 182 | } 183 | .feedList .userFeed .userInfo image{ 184 | width: 50rpx; 185 | height: 50rpx; 186 | border-radius: 50%; 187 | } 188 | .feedList .userFeed .userInfo view{ 189 | margin-left: 20rpx; 190 | width: 500rpx; 191 | height: 50rpx; 192 | } 193 | .feedList .userFeed .intro{ 194 | width: 100%; 195 | height: 160rpx; 196 | line-height: 1.7em; 197 | font-size: 32rpx; 198 | color: #676767; 199 | } 200 | .feedList .userFeed .imgBox{ 201 | margin-top: 30rpx; 202 | display: flex; 203 | width: 100%; 204 | height: 240rpx; 205 | } 206 | .feedList .userFeed .imgBox image{ 207 | width: 33%; 208 | height: 100%; 209 | margin-left: 10rpx; 210 | } 211 | .feedList .userFeed .imgBox image:first-child{ 212 | margin-left: 0; 213 | } 214 | .feedList .userFeed .tips{ 215 | display: flex; 216 | justify-content: space-between; 217 | align-items: center; 218 | margin-top: 30rpx; 219 | } 220 | .feedList .userFeed .tips .author{ 221 | background-color: #f0f2f1; 222 | padding: 8rpx; 223 | } 224 | .feedList .userFeed .tips .info{ 225 | font-size: 26rpx; 226 | } -------------------------------------------------------------------------------- /client/pages/index/index.js: -------------------------------------------------------------------------------- 1 | //index.js 2 | const qcloud = require('../../vendor/wafer2-client-sdk/index'); 3 | const config = require('../../config'); 4 | const util = require('../../utils/util.js'); 5 | 6 | Page({ 7 | data: { 8 | bannerUrls: ['https://gw.alicdn.com/tfs/TB1MEhHoN6I8KJjy0FgXXXXzVXa-750-234.jpg_760x760q75.jpg_.webp', 'https://gw.alicdn.com/imgextra/i3/0/TB2O9BaoMLD8KJjSszeXXaGRpXa_!!0-travel.jpg_760x760q75.jpg_.webp', 'https://gw.alicdn.com/tfs/TB1WGnPmBTH8KJjy0FiXXcRsXXa-750-234.jpg_760x760q75.jpg_.webp','https://gw.alicdn.com/tfs/TB1vX7qoC_I8KJjy0FoXXaFnVXa-750-234.jpg_760x760q75.jpg_.webp'], 9 | types:[ 10 | { 11 | imgUrl:'https://gw.alicdn.com/tfs/TB1lw9HfgoQMeJjy0FpXXcTxpXa-225-183.png', 12 | title:'机票' 13 | }, { 14 | imgUrl: 'https://gw.alicdn.com/tfs/TB1KsxcbjihSKJjy0FiXXcuiFXa-225-183.png', 15 | title: '酒店客栈' 16 | }, { 17 | imgUrl: 'https://gw.alicdn.com/tfs/TB1YxNfbjihSKJjy0FlXXadEXXa-225-183.png', 18 | title: '火车票' 19 | }, { 20 | imgUrl: 'https://gw.alicdn.com/tfs/TB1A0hdbaagSKJjy0FhXXcrbFXa-225-183.png', 21 | title: '汽车票' 22 | }, { 23 | imgUrl: 'https://gw.alicdn.com/tfs/TB1G_E.a6ihSKJjy0FeXXbJtpXa-227-183.png', 24 | title: '用车' 25 | }, { 26 | imgUrl: 'https://gw.alicdn.com/tfs/TB1YhCPfgMPMeJjy1XcXXXpppXa-225-183.png', 27 | title: '旅游线路' 28 | }, { 29 | imgUrl: 'https://gw.alicdn.com/tfs/TB1NBJibaagSKJjy0FcXXcZeVXa-225-183.png', 30 | title: '周边游' 31 | }, { 32 | imgUrl: 'https://gw.alicdn.com/tfs/TB1wtlcbjihSKJjy0FfXXbGzFXa-225-183.png', 33 | title: '门票' 34 | }, { 35 | imgUrl: 'https://gw.alicdn.com/tfs/TB13_9IfgoQMeJjy0FpXXcTxpXa-225-183.png', 36 | title: '领里程' 37 | }, { 38 | imgUrl: 'https://gw.alicdn.com/tfs/TB1rv1IfgMPMeJjy1XbXXcwxVXa-227-183.png', 39 | title: '全部' 40 | } 41 | ], 42 | obSupermarket: ['国际机票', '签证', '国际酒店', '国际租车', '接送机', '境外上网', '景点票务', '当地玩乐'], 43 | headlines:[{ 44 | tips1:'精选', 45 | title1:'海南五大小众海湾,景色绝美还不要门票', 46 | tips2:'超赞', 47 | title2:'这里的美景随手一拍都是大片' 48 | }, { 49 | tips1: '精选', 50 | title1: '2月最佳旅行地出炉!用一场旅行犒劳努力的自己', 51 | tips2: '超赞', 52 | title2: '告诉你五天怎么玩遍贵州经典的景点' 53 | }], 54 | station:[ 55 | { 56 | name:'香港', 57 | img:'https://gw.alicdn.com/imgextra/i2/6000000007922/TB2pD1Sae2CK1JjSZFIXXa3OpXa_!!0-travel.jpg_400x400q75.jpg_.webp', 58 | intro:'初赏维港夜景,重返梦幻童年' 59 | }, { 60 | name: '日本', 61 | img: 'https://gw.alicdn.com/imgextra/i4/6000000006424/TB2OOhnXBOBJuJjy1XdXXXIXVXa_!!2-travel.png_400x400q75.jpg_.webp', 62 | intro: '第一次的邮轮之旅,去赛琳娜号' 63 | }, { 64 | name: '泰国', 65 | img: 'https://gw.alicdn.com/imgextra/i2/6000000004125/TB2XILLXhHBK1JjSZFkXXbg9VXa_!!0-travel.jpg_400x400q75.jpg_.webp', 66 | intro: '普吉亲子游,收获中泰友谊' 67 | } 68 | ], 69 | randomPlace:[ 70 | { 71 | img:'https://gw.alicdn.com/tips/TB24CFwbShlpuFjSspkXXa1ApXa_!!689896364.jpg_400x400q75.jpg_.webp', 72 | name:'爸妈放心游', 73 | intro:'品质送爸妈' 74 | }, { 75 | img: 'https://gw.alicdn.com/tips/TB2KPh1sXXXXXamXXXXXXXXXXXX_!!490728022.jpg_400x400q75.jpg_.webp', 76 | name: '江浙沪包游', 77 | intro: '江南水乡荡悠悠' 78 | }, { 79 | img: 'https://gw.alicdn.com/tips/i6/TB1ArlNoL2H8KJjy1zkT3Br7pXa_044157.jpg_400x400q75.jpg_.webp', 80 | name: '全球游园汇', 81 | intro: '有些地方没打卡' 82 | }, { 83 | img: 'https://gw.alicdn.com/tips/T2aA4EXllaXXXXXXXX_!!503505992.jpg_400x400q75.jpg_.webp', 84 | name: '潜水老司机', 85 | intro: '旱鸭子也能潜水' 86 | }, { 87 | img: 'https://gw.alicdn.com/i2/T1PSH_FdNcXXXXXXXX_!!0-item_pic.jpg_400x400q75.jpg_.webp', 88 | name: '泡泡温泉', 89 | intro: '泡泡温泉泡泡你' 90 | }, { 91 | img: 'https://gw.alicdn.com/imgextra/i3/6000000004210/TB2suRdao3iyKJjSspnXXXbIVXa_!!0-travel.jpg_400x400q75.jpg_.webp', 92 | name: '天堂海岛游', 93 | intro: '享受马尔代夫自然美景' 94 | } 95 | ], 96 | hotPlace:[ 97 | { 98 | name: '巴厘岛', 99 | img: 'https://gw.alicdn.com/imgextra/i3/6000000004600/TB2CtiIuhRDOuFjSZFzXXcIipXa_!!0-travel.jpg_760x760q50.jpg_.webp', 100 | num: '46.7' 101 | }, { 102 | name: '武汉', 103 | img: 'https://gw.alicdn.com/imgextra/i3/6000000001348/TB2_ENirb0kpuFjy0FjXXcBbVXa_!!2-travel.png_540x540q50.jpg_.webp', 104 | num: '84.3' 105 | }, { 106 | name: '越南', 107 | img: 'https://gw.alicdn.com/imgextra/i2/6000000006173/TB2_TJbvYtlpuFjSspfXXXLUpXa_!!0-travel.jpg_760x760q50.jpg_.webp', 108 | num: '53' 109 | }, { 110 | name: '上海', 111 | img: 'https://gw.alicdn.com/imgextra/i4/6000000006745/TB235SueLNZWeJjSZFpXXXjBFXa_!!0-travel.jpg_760x760q50.jpg_.webp', 112 | num: '78.9' 113 | } 114 | ], 115 | youlike:[], 116 | loadTimes:0, 117 | perLoad:4, 118 | likePlace:[ 119 | { 120 | type:'三亚 | 自由行', 121 | img:'https://gw.alicdn.com/bao/uploaded/i4/2256687003/TB2NTLRbTTI8KJjSsphXXcFppXa_!!2256687003.jpg_400x400q75.jpg_.webp', 122 | title:'海南三亚5天4晚自由行 希尔顿泳池房连住 蜈支洲酒店泳池别墅自驾', 123 | price:'1999', 124 | peopleNum:'1934' 125 | }, { 126 | type: '迪拜 | 跟团游', 127 | img: 'https://gw.alicdn.com/bao/uploaded/i3/3190378410/TB28Gs5bgnD8KJjy1XdXXaZsVXa_!!3190378410.jpg_400x400q75.jpg_.webp', 128 | title: '迪拜旅游帆船酒店5678星定制7天私家团旅行含门票餐厅包车直升机', 129 | price: '37880', 130 | peopleNum: '154' 131 | }, { 132 | type: '塞班 | 自由行', 133 | img: 'https://gw.alicdn.com/bao/uploaded/i2/2935198750/TB2a1..fL6H8KJjy0FjXXaXepXa_!!2935198750.jpg_400x400q75.jpg_.webp', 134 | title: '春节出发美国塞班岛自由行5-6天含机票酒店赠送环岛+军舰岛旅游', 135 | price: '2999', 136 | peopleNum: '14' 137 | }, { 138 | type: '巴黎 | 跟团游', 139 | img: 'https://gw.alicdn.com/i4/754920082/TB2BUIWnbplpuFjSspiXXcdfFXa_!!754920082.jpg_400x400q75.jpg_.webp', 140 | title: '海南三亚5天4晚自由行 希尔顿泳池房连住 蜈支洲酒店泳池别墅自驾', 141 | price: '6978', 142 | peopleNum: '194' 143 | }, { 144 | type: '甘孜 | 跟团游', 145 | img: 'https://gw.alicdn.com/bao/uploaded/i7/TB1mKvOdMvD8KJjSsplTAGIEFXa_121140.jpg_400x400q75.jpg_.webp', 146 | title: '四川成都到海螺沟旅游燕子沟3天2晚纯玩跟团三日游温泉游中青旅', 147 | price: '299', 148 | peopleNum: '434' 149 | }, { 150 | type: '东京 | 自由行', 151 | img: 'https://gw.alicdn.com/bao/uploaded/i1/2438506708/TB2xKvZayqAXuNjy1XdXXaYcVXa_!!2438506708.jpg_400x400q75.jpg_.webp', 152 | title: '北京直飞东京大阪5-7天特价机票自由行 五星全日空往返 日本旅游', 153 | price: '2499', 154 | peopleNum: '634' 155 | }, { 156 | type: '香港 | 跟团游', 157 | img: 'https://gw.alicdn.com/bao/uploaded/i3/2120928523/TB2RlJjXNjxQeBjy1zbXXbqApXa_!!2120928523.jpg_400x400q75.jpg_.webp', 158 | title: '春节抢全国双飞港澳游5天4晚自由行迪士尼海洋公园香港澳门旅游团', 159 | price: '899', 160 | peopleNum: '1854' 161 | }, { 162 | type: '西安 | 跟团游', 163 | img: 'https://gw.alicdn.com/i3/2697550341/TB2Jey3qVXXXXbzXpXXXXXXXXXX_!!2697550341.jpg_400x400q75.jpg_.webp', 164 | title: '飞猪专线陕西西安旅游兵马俑一日游纯玩跟团游含华清池兵马俑门票', 165 | price: '235', 166 | peopleNum: '4676' 167 | } 168 | ] 169 | }, 170 | 171 | loadMore:function(){ 172 | const loadTimes = this.data.loadTimes; 173 | const perLoad = this.data.perLoad; 174 | let temArr = this.data.likePlace.slice((loadTimes * perLoad), (loadTimes*perLoad)+perLoad); 175 | if(temArr.length<=0){ 176 | return; 177 | } 178 | 179 | wx.showLoading({ 180 | title: '加载中', 181 | }); 182 | setTimeout(() => { 183 | let youlikeArr = this.data.youlike.slice(); 184 | youlikeArr.push(...temArr); 185 | this.setData({ 186 | youlike: youlikeArr, 187 | loadTimes: loadTimes + 1 188 | },()=>{ 189 | wx.hideLoading(); 190 | }) 191 | },1000); 192 | }, 193 | 194 | onLoad: function (options) { 195 | // Do some initialize when page load. 196 | }, 197 | onReady: function () { 198 | }, 199 | onShow: function () { 200 | // Do something when page show. 201 | }, 202 | onHide: function () { 203 | // Do something when page hide. 204 | }, 205 | onUnload: function () { 206 | // Do something when page close. 207 | }, 208 | onPullDownRefresh: function () { 209 | // Do something when pull down. 210 | }, 211 | onReachBottom: function () { 212 | this.loadMore(); 213 | } 214 | }) 215 | -------------------------------------------------------------------------------- /client/pages/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": { 3 | "section-title": "../../components/SectionTitle/SectionTitle" 4 | } 5 | } -------------------------------------------------------------------------------- /client/pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 赶快输入目的地~ 7 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {{item.title}} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 一站汇集全球玩乐 34 | 35 | 更多 > 36 | 37 | 38 | 39 | {{item}} 40 | 41 | 42 | 43 | 44 | 45 | 47 | 48 | 49 | 50 | 51 | {{item.tips1}} 52 | {{item.title1}} 53 | 54 | 55 | {{item.tips2}} 56 | {{item.title2}} 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 特价!昆大丽6天5晚含机票品质跟团游 67 | 1780元 Go> 68 | 69 | 70 | 冬季出行 71 | 特价1折起 72 | 73 | 74 | 吃喝玩乐尽在酒店 75 | NEW 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 推荐 85 | 沉醉冰岛极光,初遇冰与火之国 86 |
87 | 1人说好 88 |
89 | 90 |
91 | 92 | 93 | 94 | 95 | {{item.name}} 96 | 97 | {{item.intro}} 98 | 99 | 100 |
101 |
102 | 103 | 104 | 105 | 106 | 107 | 108 | {{item.name}} 109 | {{item.intro}} 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 三亚 121 | 90.1万人去过 122 | 123 | 124 | 125 | 126 | 127 | {{item.name}} 128 | {{item.num}}万人去过 129 | 130 | 131 | 132 | 133 | 134 | 150 |
151 |
-------------------------------------------------------------------------------- /client/pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | /**index.wxss**/ 2 | .header{ 3 | background-color: #5fca9c; 4 | width: 100%; 5 | height: 100rpx; 6 | position: fixed; 7 | top: 0; 8 | left: 0; 9 | z-index: 10000; 10 | } 11 | .header .searchBar{ 12 | width: 90%; 13 | height: 70rpx; 14 | padding-left: 20rpx; 15 | border-radius: 10rpx; 16 | margin: auto; 17 | position: relative; 18 | top: 50%; 19 | margin-top: -35rpx; 20 | background-color: rgba(255, 255, 255, 0.9); 21 | display: flex; 22 | align-items: center; 23 | } 24 | .header .searchBar text{ 25 | font-size: 26rpx; 26 | margin-left: 10rpx; 27 | color: #b5b3b2; 28 | } 29 | .content{ 30 | width: 100%; 31 | padding-top: 100rpx; 32 | } 33 | .bannerSwiper{ 34 | width: 100%; 35 | height: 250rpx; 36 | } 37 | .bannerSwiper image{ 38 | width: 100%; 39 | height: 100%; 40 | } 41 | 42 | .types{ 43 | width: 100%; 44 | height: 300rpx; 45 | background-color: white; 46 | display: flex; 47 | flex-wrap: wrap; 48 | } 49 | .types .typeItem{ 50 | width: 20%; 51 | height: 50%; 52 | display: flex; 53 | flex-direction: column; 54 | justify-content: center; 55 | align-items: center; 56 | } 57 | .types .typeItem image{ 58 | width: 120rpx; 59 | height: 100rpx; 60 | } 61 | .types .typeItem text{ 62 | font-size: 26rpx; 63 | } 64 | 65 | .obSupermarket{ 66 | width: 100%; 67 | height: 250rpx; 68 | background-color: white; 69 | } 70 | .obSupermarket .obHeader{ 71 | width: 100%; 72 | height: 80rpx; 73 | display: flex; 74 | align-items: center; 75 | justify-content: space-between; 76 | padding: 0 30rpx; 77 | background: linear-gradient(rgba(225, 236, 115, 0.3),#fff); 78 | } 79 | .obSupermarket .obHeader .left .logo{ 80 | width: 140rpx; 81 | height: 36rpx; 82 | display: inline-block; 83 | vertical-align: middle; 84 | background-size: 100% 100%; 85 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAYMAAABaCAMAAABt/MpzAAAAjVBMVEUAAAA9PT09PT0/Pz89PT09PT0/Pz9HR0c9PT09PT09PT1DQ0M9PT09PT09PT09PT09PT09PT0+Pj49PT0/Pz89PT09PT09PT0+Pj5BQUE+Pj4+Pj49PT09PT1AQEA9PT09PT37wAA9PT3/wwDxuQO9lhREQjrYqAtOSTd0ZCqRdyKqiRlcUjI9PT3/wwD8bAA4AAAALXRSTlMA+M83rr4iCHNW6RJ71o5ohZlAtzDhgPBJGFxPxp8qYtzipfHQwPbD7NHGweCPnW0gAAALD0lEQVR42u2dbWOiuhKAJ7yFFwVEFFFEatvt2bM7+/9/3u25PduJTSIhpGtPd59vrS1KHjKTTALCB6RODhH84YYkAz4T1PCHW7HFF1gFf7gNB/xJ08MfbsIGX+nAnvaUF7dy6Bf5KYL/LjUKnMCWfYPPlHADsgD/oeTgmuToBYsM3p09CqS2b+jjCzH8criHLxzBLe36pU12YA3X/T67oECRVSYBJnT4AuPwqynxJztwyhpfYBFYsQsb5m05SCyWOBHWHcZ9M/wXH2yJogxsGBDfpSMk+JM12JDj/1lKBkO0YTvafviTBPT022Cl8ZkEKSI2q+kKM3zFg2n0pyQCLSu6CkFHeyj8sRgfwiULtMM3drAHLQXDZ8IWJE7UOXOYSCRkM5hCG+AzKw4aPHylBzUxQ0SvBhWpLkQu0Qq5Zap1iuMsV5mctocILuk7FFg7cWCewgLQsBy9BON/37a9/rFKEOFoSSi9uyFpL4fB1AcBXjKDwOfeQUGXqaWDluELK93MV2k5RQvkbFczNIWmfQ3FV+G0K7lr1m/H6FcR/p95o4T5z6Of6TK1c0DNPIBMou3ZZ7RjL6V8U6hFmWIumB1HAh8P0DEsfhPuA1sHFAwmOWgbJ6EotNEXygEnSVFBKtl2i39bB9B7NpEoA+vuROG2QpEVQLTWdx2ad7gnuJEDot8X01Cs7pzQGJZpEnkQM9RwEMdS7klv64Bo82ETJqCmWm82qx70dGhKLA1FdFDkL8SCons2H8RBv9HMiKitmmpkfmOaAIkDQx3DDphidMrQPecP4sCjS05ihy+kHPTUp4VATie4IOJDC5f4qcZVyUHpIEbnsP62DqT82MhzPD7Iqwkz60VENKCCrgdQO3A/MGoO8DEcnMTZl7amVLp3AK0nt0oBoHUA/uIcXEMYXLFglGPRwkdzIB8/Yu/qAPgaLzm3IDl4/1rF7R1kTLuGLMxca/cO6B0oF8Nv5aBeBP/S4CvBJUx+paycOoDyIhf/Xg5KtOXIHTqgiUJXA/xeDk5oT+nUARwY5eIbOQipiewc0CXNzB2kOIPMsCEOYITfYNDCDR1QzWtp6+A4vXYd4Rwqtw6g3wHc1AFNPFpLBxvNsCYO06YR0iprmmZ5jOY7kJos8gVok9LCF8hghBs6oGi+tXOwE4sfBO9QCUvgmcZhLIrNDub5H9YB/R+LbBxwen1vtL7VtGTeipUUCg3ZuXXAJXrBAVcA42v2XmbhIJDK82NVxphazoaQS93QkA135cAvuw3D6aTeOeFXZym4rKc6aEPNKmM/Vq/drTpvOt1xD5esHG1NyupDES/K/AWauudETPepzIDlmWpbBHE+tOYOsipnuq7eopYcXBKiOSdN8x/KsDFeeIw6nElz0nQEgl1i9Iq82D6gjgRm4bgfZMWaoRkLeKZKcT659ClSdEENZnG6Aw3cNI1Z5APdQlB9ZNPGxBVztn5GuDpuLNWkG1SxbkGC71bhwFAJa7zFVRNn+3FRH+AUOEBEJ+W4uQqczREk2qTMc+Esh3/Smg8S0XH0xJY+6FmQvUnH4CuchEfVNQf4ckqYScCBmLR+kOXM6A0i0NNXAieK35WA1P38YXqtcI/OkEPyns3NMZYO/A2a0ezAjNqsXlTYJLwNukM+H3+J1qR7sHWQTHAfu3SQ41SWABUSXh5LlOJOGgXbgKnSMnFK0Qq25WDrIMEpLNw5yHEyhfhfTWJXL4pCOgQo4Mm5mSxgXWQAtg58hiaMNmofpKxLJAc7fRqXT+Tr49PTw8PdWx5fm4yLGflgW7Pjw+isvd4V2/ISoXscS5FtcfA5EJMdZKnNliiZHaOkJDkwmVP89e3h/oeav8VI2NB81NYBJCNzVYu6qYUDu4BAh9CqrAwdROmlgUcSIPHX6/WX50KZ395BhGT19g4ihgQLyviFTvhlGHTp6BLanrKcmYMARciAggcUWFHhycKBXJK9pQN5gtvEXLHdaJm97MoWslR3Lb8uzRzsLjrBww9CGYqIglZKPokDTo296VXreixS7EmMrlTtNmYOlkh8v7uq4I6hQP3pHOzUz8nZKIJuS7+MZzs4iL2AFCj5igJe9OkcrJTtWimX5ajdwtkOQnzly8N1BY8oEn8+Bx2NvJVJIlfuRGsmOIgrws/kdsLHH9d5nh9QQmDZOzros5s42Kgm7RlTR/4tFZBNHOhvLIBYiET3P0b5LlwT7+WArxjisrqBA+X1EGuCTkJqbB0gq9+EoqdxBQ/03+37OKCHHh3mO6jjVRGZO+AUMpRvlmgSeG3tAL2LJsAvBt2AQtEC3svBgpb57B3QKJ0VFv2gVI6VuOZGkdbeAbYXr/49pRss+Xs5oFy3m+WAgkhl7GBQRJ2Ooq9uEAUzHFRk2SgU3X8XKlXv5YB+Ec9yQBMuz9hBSJGWhqCakMNTOrd5/eBEPz2MKqDpQSKVetw5oHA3z4H/2qDc1EEprUXzja4mQak6mOGgu6xa342NTL+/qQ/11Es/lAN53GLkgLRR1+ahbqXAZ/SnVg4onuT044iCxy+vf3l6+4lXn8SBuDh79qP+NIjrhiKHRpgoWTsIozf7XEccvCoYfPi0DrZmzyjyAyTOMOqAWir2Bbi0ZjEyNBW2jHxeBzxFDU34itegAIumONiBTGyaD+7oevjEDqCw3UNj78B8XPRAJ/2ZHUCI01jy2Q4OhhU7qpm2n9tBNuAUmggmOahAhl7Fr0ZLBww+twPoUzSHVTDfAWdGSZlW0DzXDqjJb+qAiJZoysYHBw4gNAtG3ygFuXVQ08Tvtg4I3qEZXQtOHGzRpHB6/4XO2a2DktLVDRyEp7MXFNJ10aABQwLgxoGPxLfxmnUKbh34jFrqFzsgugxEfAMFbH3i4MoBb3C8dPpEtSynDrKYFCzlIr6Fg/H6PjkgVvpbgJYhMVDNUhJgWKs4aILB6KL+wxfy6MbB/uw9s0GBUiriny0cmDw3JEGJTPdwxVK9LRdgkoN2ZEdixkQJT6pewCh+ggsHfI0SLJIfgD7PgUftMOqg0tx5EoMTB9S3B26w7/3b/dt0/A0J34mD/OqU/0gjpTkOKt3+nzpACV9dqyjAkQNPGcyJll3Z8nv/9BcSAbhwwFFmkyku0zSyd5ANyouZnzyUYVz5vNA9zHcgjT69QwvAszfEeMmXr48Pd/c/7u8enr4y7V57+zUcHyVYrZw2Dr2VA/1+0NpDFbGqdM0O+gJnNNFBja5IQLMVZG/hQH+6gfBCGdk4yOJUuPBG7/AoVTGS7a6Mdoe8FNjuoxEHsEY3lNrvlcmmxSKGl6S+NHUmhlCGCS0cyixR5HA9EbFjTQ1HNAq5PupgC60Dpx1hrb3TIQBjB9SniWMrPQnNIZ1iqER4Rab+RsAaFKSoJdY7IP2ub7mmA3fc2IH8gZpzrboxyBmsBmJ42wV8TQOn/dRbJ1mmc+AuGm1BRbXqzieASQ7oKc3HPK7Uh2XoADlVBVIXICIkhgiUZA1q2Y04yEKcB0vAAsmBMQW+Sw7zxS6g/7DLFjScTGTL90KRGHuOEfxaB7Bn7/Ioki11AYlGen7bpGjUS66kVHlYoi1BD5ZIRQ5z/NTZs/xFqmDjrXTPBqYqqp6F0XdDRUw9at8HDKfjbWsXDxUrYSq8ZDgLtmphEkcae1yh6lDBJlIG00A+LT+JF2Pkgbdh+Eyz6Y6nFuZQzfry57ZcojWbPIKp7FbhcQej+Hm3YRd4uSTuMCA2W5gDz8AFC5pzWhEV5XEdTuWcxzXcnDaCj0HVMWyCCP5wS/hEAf8DdFjwwPaRSe0AAAAASUVORK5CYII=); 86 | } 87 | .obSupermarket .obHeader .left text{ 88 | margin-left: 30rpx; 89 | display: inline-block; 90 | vertical-align: middle; 91 | color: #a5a5a5; 92 | } 93 | .obSupermarket .obHeader .right{ 94 | color: black; 95 | } 96 | 97 | .obSupermarket .obContent{ 98 | display: flex; 99 | flex-wrap: wrap; 100 | width: 100%; 101 | height: 170rpx; 102 | } 103 | .obSupermarket .obContent view{ 104 | width: 25%; 105 | height: 50%; 106 | text-align: center; 107 | } 108 | .obSupermarket .obContent text{ 109 | border: 1px solid #d2e32d; 110 | vertical-align: middle; 111 | display: inline-block; 112 | width: 80%; 113 | height: 60rpx; 114 | line-height: 60rpx; 115 | text-align: center; 116 | border-radius: 30rpx; 117 | } 118 | 119 | .headlines{ 120 | width: 100%; 121 | height: 140rpx; 122 | background-color: white; 123 | margin-top: 20rpx; 124 | display: flex; 125 | align-items: center; 126 | justify-content: flex-start; 127 | padding: 0; 128 | } 129 | .headlines .logo{ 130 | width: 160rpx; 131 | height: 120rpx; 132 | overflow: hidden; 133 | background-image: url(https://gw.alicdn.com/tfs/TB1Lm5gRXXXXXcOXFXXXXXXXXXX-200-150.png_160x160q75.jpg_.webp); 134 | background-size: 100%; 135 | } 136 | .headlines .headlineSwiper{ 137 | flex:1; 138 | height: 100%; 139 | } 140 | .headlines .headlineSwiper .headline{ 141 | width: 100%; 142 | height: 50%; 143 | display: flex; 144 | align-items: center; 145 | } 146 | .headlines .headlineSwiper .headline view{ 147 | color:red; 148 | border: 1px solid red; 149 | border-radius: 6rpx; 150 | width: 60rpx; 151 | } 152 | .headlines .headlineSwiper .headline text{ 153 | margin-left:10rpx; 154 | flex:1; 155 | } 156 | 157 | .barginPrice{ 158 | width: 100%; 159 | height: 260rpx; 160 | background-color: white; 161 | margin-top: 20rpx; 162 | display: flex; 163 | align-items: center; 164 | padding: 0 30rpx; 165 | } 166 | .barginPrice .hotSale{ 167 | width: 320rpx; 168 | height: 240rpx; 169 | position: relative; 170 | background-image: url(https://gw.alicdn.com/imgextra/TB14dARoCfD8KJjSszhSuvIJFXa.jpg_400x400q90.jpg_.webp); 171 | background-size: 100% 100%; 172 | } 173 | .barginPrice .hotSale .title{ 174 | position: absolute; 175 | top: 0; 176 | left:0; 177 | width: 100%; 178 | height: 100%; 179 | background-image: url(https://gw.alicdn.com/tfs/TB1hEoaXMvGK1Jjy0FeXXXYupXa-518-384.png); 180 | background-size: 100% 100%; 181 | } 182 | .barginPrice .hotSale .name{ 183 | position: absolute; 184 | top: 80rpx; 185 | left: 20rpx; 186 | color:white; 187 | width: 75%; 188 | } 189 | .barginPrice .hotSale .price{ 190 | position: absolute; 191 | top: 170rpx; 192 | background-color: #ff5000; 193 | border-bottom-right-radius: 30rpx; 194 | color:white; 195 | width: 60%; 196 | padding-left: 20rpx; 197 | height: 50rpx; 198 | line-height: 45rpx; 199 | } 200 | .barginPrice .ticket{ 201 | width: 170rpx; 202 | height: 240rpx; 203 | position: relative; 204 | background-size: 100% 100%; 205 | margin-left: 20rpx; 206 | } 207 | .barginPrice .ticket1{ 208 | background-image: url(https://gw.alicdn.com/tfs/TB1eGFZSFXXXXcdaXXXXXXXXXXX-251-384.png_400x400q90.jpg_.webp); 209 | } 210 | .barginPrice .ticket2{ 211 | background-image: url(https://gw.alicdn.com/tfs/TB1jtUISFXXXXcrXFXXXXXXXXXX-249-384.png_400x400q90.jpg_.webp); 212 | } 213 | .barginPrice .ticket .title{ 214 | position: absolute; 215 | top: 80rpx; 216 | left: 10rpx; 217 | font-size: 24rpx; 218 | } 219 | .barginPrice .ticket .tips{ 220 | position: absolute; 221 | top: 150rpx; 222 | left: 10rpx; 223 | color:#ff5000; 224 | font-size: 24rpx; 225 | } 226 | 227 | .section{ 228 | margin-top: 20rpx; 229 | width: 100%; 230 | background-color: white; 231 | } 232 | .section .contentWrapper{ 233 | padding: 0 20rpx; 234 | } 235 | .firstStation .contentWrapper .hot{ 236 | width: 100%; 237 | height: 260rpx; 238 | display: flex; 239 | } 240 | .firstStation .contentWrapper .hot .hotLeft{ 241 | width: 30%; 242 | height: 100%; 243 | display: flex; 244 | flex-direction: column; 245 | background-color: #f2f3f4; 246 | padding-left: 10rpx; 247 | } 248 | .firstStation .contentWrapper .hot .hotLeft .tips{ 249 | background-color: #ffc900; 250 | color: black; 251 | width: 80rpx; 252 | height: 60rpx; 253 | text-align: center; 254 | line-height: 60rpx; 255 | } 256 | .firstStation .contentWrapper .hot .hotLeft .text{ 257 | font-size: 32rpx; 258 | line-height: 1.2em; 259 | margin-top: 20rpx; 260 | color: #1e1e1e; 261 | } 262 | .firstStation .contentWrapper .hot .hotLeft .people{ 263 | margin-top: 10rpx; 264 | padding-top: 10rpx; 265 | border-top: 1px solid #ffc900; 266 | width: 120rpx; 267 | } 268 | .firstStation .contentWrapper .hot .hotRight{ 269 | width: 70%; 270 | height: 100%; 271 | } 272 | .firstStation .contentWrapper .stations{ 273 | margin-top: 10rpx; 274 | display: flex; 275 | justify-content: space-between; 276 | } 277 | .firstStation .contentWrapper .stations .stationItem{ 278 | width: 230rpx; 279 | height: 280rpx; 280 | } 281 | .firstStation .contentWrapper .stations .stationItem .stationImg{ 282 | position: relative; 283 | width: 100%; 284 | height: 140rpx; 285 | } 286 | .firstStation .contentWrapper .stations .stationItem .stationImg image{ 287 | position: absolute; 288 | top: 0; 289 | left: 0; 290 | width: 100%; 291 | height: 100%; 292 | } 293 | .firstStation .contentWrapper .stations .stationItem .stationImg view{ 294 | position: absolute; 295 | bottom: 0; 296 | left: 0; 297 | color: white; 298 | background-color: rgba(0, 0, 0, 0.5); 299 | text-align: center; 300 | height: 40rpx; 301 | line-height: 40rpx; 302 | padding: 0 20rpx; 303 | } 304 | .firstStation .contentWrapper .stations .stationItem .stationTitle{ 305 | width: 100%; 306 | margin-top: 10rpx; 307 | padding-right: 10rpx; 308 | color:black; 309 | } 310 | 311 | .randomPlace .contentWrapper{ 312 | display: flex; 313 | flex-wrap: wrap; 314 | justify-content: space-between; 315 | } 316 | .randomPlace .placeItem{ 317 | width: 230rpx; 318 | height: 280rpx; 319 | } 320 | .randomPlace .placeItem image{ 321 | width: 100%; 322 | height: 140rpx; 323 | } 324 | .randomPlace .placeItem .title{ 325 | color: black; 326 | margin-top: 10rpx; 327 | font-size: 32rpx; 328 | display: block; 329 | } 330 | .randomPlace .placeItem .tips{ 331 | margin-top: 10rpx; 332 | display: block; 333 | } 334 | 335 | .hotPlace .contentWrapper{ 336 | display: flex; 337 | justify-content: space-between; 338 | height: 300rpx; 339 | } 340 | .hotPlace .contentWrapper .left{ 341 | width: 33%; 342 | height: 100%; 343 | } 344 | .hotPlace .contentWrapper .right{ 345 | height: 100%; 346 | width: 66%; 347 | display: flex; 348 | flex-wrap: wrap; 349 | justify-content: space-between; 350 | align-content: space-between; 351 | } 352 | .hotPlace .contentWrapper .right .placeItem{ 353 | width: 49%; 354 | height: 49%; 355 | } 356 | .hotPlace .contentWrapper .placeItem{ 357 | position: relative; 358 | } 359 | .hotPlace .contentWrapper .placeItem image{ 360 | position: absolute; 361 | width: 100%; 362 | height: 100%; 363 | top: 0; 364 | left: 0; 365 | } 366 | .hotPlace .contentWrapper .placeItem .bg{ 367 | width: 100%; 368 | height: 100%; 369 | position: absolute; 370 | top: 0; 371 | left: 0; 372 | background: linear-gradient(180deg,transparent 0,rgba(0,0,0,.3) 50%); 373 | } 374 | .hotPlace .contentWrapper .placeItem .placeName{ 375 | color: white; 376 | position: absolute; 377 | top: 16rpx; 378 | left: 10rpx; 379 | font-size: 38rpx; 380 | } 381 | .hotPlace .contentWrapper .placeItem .placeNum{ 382 | color: white; 383 | position: absolute; 384 | top: 80rpx; 385 | left: 10rpx; 386 | } 387 | 388 | .like .contentWrapper{ 389 | background-color: #f6f6f6; 390 | display: flex; 391 | flex-wrap: wrap; 392 | justify-content: space-between; 393 | } 394 | .like .contentWrapper .likeItem{ 395 | width: 49%; 396 | height: 540rpx; 397 | background-color: white; 398 | margin-bottom: 20rpx; 399 | } 400 | .like .contentWrapper .likeItem .imgBox{ 401 | position: relative; 402 | width: 100%; 403 | height: 350rpx; 404 | } 405 | .like .contentWrapper .likeItem .imgBox image{ 406 | width: 100%; 407 | height: 100%; 408 | position: absolute; 409 | top: 0; 410 | left: 0; 411 | } 412 | .like .contentWrapper .likeItem .imgBox .type{ 413 | position: absolute; 414 | top: 10rpx; 415 | left: 20rpx; 416 | color:white; 417 | background-color: rgba(0, 0, 0, 0.8); 418 | padding: 10rpx; 419 | border-radius: 20rpx; 420 | border-top-right-radius: 0; 421 | } 422 | .like .contentWrapper .likeItem .title{ 423 | color:black; 424 | margin-top: 20rpx; 425 | padding: 0 10rpx; 426 | } 427 | .like .contentWrapper .likeItem .info{ 428 | margin-top: 20rpx; 429 | padding: 0 10rpx; 430 | display: flex; 431 | justify-content: space-between; 432 | align-items: center; 433 | } 434 | .like .contentWrapper .likeItem .info .price{ 435 | color:#ff5000; 436 | font-size: 36rpx; 437 | } 438 | .like .contentWrapper .likeItem .info .num{ 439 | font-size: 26rpx; 440 | } -------------------------------------------------------------------------------- /client/pages/me/me.js: -------------------------------------------------------------------------------- 1 | // pages/me/me.js 2 | const util = require('../../utils/util.js'); 3 | const recorderManager = wx.getRecorderManager(); 4 | const innerAudioContext = wx.createInnerAudioContext(); 5 | 6 | Page({ 7 | 8 | /** 9 | * 页面的初始数据 10 | */ 11 | data: { 12 | userImg:'', 13 | userName:'', 14 | uploadImgList:[], 15 | recordFilePath:'', 16 | pausing:false, 17 | }, 18 | 19 | //选择图片 20 | chooseImg:function(){ 21 | wx.chooseImage({ 22 | count: 9, 23 | sizeType: ['original', 'compressed'], 24 | sourceType: ['album', 'camera'], 25 | success: (res)=>{ 26 | this.setData({ 27 | uploadImgList:res.tempFilePaths 28 | }) 29 | }, 30 | fail:()=>{ 31 | util.showModel('上传图片失败','请稍后重试') 32 | } 33 | }) 34 | }, 35 | //删除图片 36 | deleteImg:function(e){ 37 | const img = e.currentTarget.dataset.img; 38 | let temArr = this.data.uploadImgList.slice(); 39 | for(let i=0;i { 56 | 57 | }, 58 | fail: () => { 59 | util.showModel('预览图片失败', '请稍后重试'); 60 | } 61 | }) 62 | }, 63 | //开始录音 64 | startRecord:function(e){ 65 | recorderManager.start(); 66 | }, 67 | //暂停录音 68 | pauseRecord: function(e){ 69 | recorderManager.pause(); 70 | }, 71 | //继续录音 72 | resumeRecord: function (e) { 73 | recorderManager.resume(); 74 | }, 75 | //停止录音 76 | stopRecord: function(e){ 77 | recorderManager.stop(); 78 | }, 79 | //播放录音 80 | playRecord:function(e){ 81 | innerAudioContext.src = this.data.recordFilePath; 82 | innerAudioContext.play(); 83 | }, 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | /** 98 | * 生命周期函数--监听页面加载 99 | */ 100 | onLoad: function (options) { 101 | 102 | }, 103 | 104 | /** 105 | * 生命周期函数--监听页面初次渲染完成 106 | */ 107 | onReady: function () { 108 | //录音回调 109 | recorderManager.onPause(() => { 110 | this.setData({ 111 | pausing: true 112 | }) 113 | }); 114 | recorderManager.onResume(() => { 115 | this.setData({ 116 | pausing: false 117 | }) 118 | }); 119 | recorderManager.onFrameRecorded((res) => { 120 | this.setData({ 121 | recordFilePath: res.tempFilePath, 122 | pausing: false 123 | }) 124 | }); 125 | recorderManager.onStop((res) => { 126 | this.setData({ 127 | recordFilePath: res.tempFilePath, 128 | pausing: false 129 | }) 130 | }); 131 | 132 | 133 | //获取用户信息 134 | wx.getUserInfo({ 135 | success: (res)=>{ 136 | const userInfo = res.userInfo; 137 | const nickName = userInfo.nickName; 138 | const avatarUrl = userInfo.avatarUrl; 139 | const gender = userInfo.gender; //性别 0:未知、1:男、2:女 140 | const province = userInfo.province; 141 | const city = userInfo.city; 142 | const country = userInfo.country; 143 | this.setData({ 144 | userImg: avatarUrl, 145 | userName: nickName 146 | }) 147 | } 148 | }) 149 | }, 150 | 151 | /** 152 | * 生命周期函数--监听页面显示 153 | */ 154 | onShow: function () { 155 | 156 | }, 157 | 158 | /** 159 | * 生命周期函数--监听页面隐藏 160 | */ 161 | onHide: function () { 162 | 163 | }, 164 | 165 | /** 166 | * 生命周期函数--监听页面卸载 167 | */ 168 | onUnload: function () { 169 | 170 | }, 171 | 172 | /** 173 | * 页面相关事件处理函数--监听用户下拉动作 174 | */ 175 | onPullDownRefresh: function () { 176 | 177 | }, 178 | 179 | /** 180 | * 页面上拉触底事件的处理函数 181 | */ 182 | onReachBottom: function () { 183 | 184 | }, 185 | 186 | /** 187 | * 用户点击右上角分享 188 | */ 189 | onShareAppMessage: function () { 190 | 191 | } 192 | }) -------------------------------------------------------------------------------- /client/pages/me/me.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "个人中心" 3 | } -------------------------------------------------------------------------------- /client/pages/me/me.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{userName}} 6 | 7 | 8 | 9 | 选择、预览图片测试> 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 录音测试> 20 | 21 | 22 | 开始 23 | 暂停 24 | 继续 25 | 停止 26 | 播放录音 27 | 28 | 29 | 30 | 欢迎来到旅行呱~嘎嘎嘎 31 | 32 | 33 | -------------------------------------------------------------------------------- /client/pages/me/me.wxss: -------------------------------------------------------------------------------- 1 | /* pages/me/me.wxss */ 2 | .userInfo{ 3 | width: 100%; 4 | height: 220rpx; 5 | display: flex; 6 | align-items: center; 7 | background-color: #5fca9c; 8 | padding-left: 40rpx; 9 | } 10 | .userInfo image{ 11 | width: 120rpx; 12 | height: 120rpx; 13 | border-radius: 50%; 14 | } 15 | .userInfo text{ 16 | margin-left: 30rpx; 17 | font-size: 36rpx; 18 | color: white; 19 | } 20 | 21 | .normalBar{ 22 | width: 100%; 23 | height: 100rpx; 24 | background: white; 25 | display: flex; 26 | justify-content: space-between; 27 | align-items: center; 28 | padding: 30rpx; 29 | font-size: 35rpx; 30 | margin-top: 20rpx; 31 | } 32 | .imgList{ 33 | width: 100%; 34 | display: flex; 35 | flex-wrap: wrap; 36 | justify-content: flex-start; 37 | padding: 20rpx; 38 | } 39 | .imgList .imgBox{ 40 | position: relative; 41 | width: 230rpx; 42 | height: 230rpx; 43 | margin-bottom: 10rpx; 44 | margin-right: 10rpx; 45 | } 46 | .imgList .imgBox:nth-of-type(3n){ 47 | margin-right: 0; 48 | } 49 | .imgList .imgBox image{ 50 | position: absolute; 51 | top: 0; 52 | left: 0; 53 | width: 100%; 54 | height: 100%; 55 | z-index: 1; 56 | } 57 | .imgList .imgBox icon{ 58 | position: absolute; 59 | top: -10rpx; 60 | right: -10rpx; 61 | z-index: 10; 62 | } 63 | 64 | .controlBox{ 65 | width: 100%; 66 | height: 120rpx; 67 | display: flex; 68 | justify-content: space-around; 69 | align-items: center; 70 | } 71 | .controlBox view{ 72 | width: 150rpx; 73 | height: 60rpx; 74 | text-align: center; 75 | line-height: 60rpx; 76 | border: 2rpx solid #807d7a; 77 | border-radius: 6rpx; 78 | font-size: 32rpx; 79 | } 80 | 81 | 82 | .welcome{ 83 | margin-top: 50rpx; 84 | } 85 | .welcome text{ 86 | font-size: 38rpx; 87 | color: #5fca9c; 88 | } -------------------------------------------------------------------------------- /client/pages/search/search.js: -------------------------------------------------------------------------------- 1 | // pages/search/search.js 2 | const util = require('../../utils/util.js'); 3 | 4 | Page({ 5 | data: { 6 | inputVal:'', 7 | hotPlaces: ['泰国', '台湾', '三亚', '厦门', '云南', '北京', '新加坡', '马尔代夫', '柬埔寨', '越南', '哈尔滨', '佛山', '西安', '英国', '土耳其'], 8 | historyPlaces:[], 9 | mockPlaces: ['测试词汇', '测试词汇', '测试词汇'], 10 | suggestPlaces: [], 11 | clientHeight:util.getClientSize().height+'px', 12 | }, 13 | 14 | //获取input的value 15 | setValue:function(e){ 16 | if(e.detail.value.trim().length>0){ 17 | this.setData({ 18 | suggestPlaces:this.data.mockPlaces, 19 | inputVal: e.detail.value 20 | }); 21 | }else{ 22 | this.setData({ 23 | suggestPlaces: [], 24 | inputVal: e.detail.value 25 | }); 26 | } 27 | 28 | }, 29 | 30 | //搜索 31 | searchConfirm:function(){ 32 | //保存搜索记录到本地 33 | let curArr = wx.getStorageSync("historySearch"); 34 | if (!curArr || !(curArr instanceof Array)){ 35 | curArr = []; 36 | } 37 | curArr.push(this.data.inputVal); 38 | curArr = [...new Set(curArr)]; 39 | wx.setStorageSync('historySearch', curArr); 40 | 41 | util.showSuccess('搜索'+this.data.inputVal); 42 | }, 43 | 44 | //直接点击目的地 45 | searchDirectly:function(e){ 46 | this.setData({ 47 | inputVal: e.target.dataset.place 48 | },()=>{ 49 | this.searchConfirm(); 50 | }) 51 | }, 52 | 53 | //清除历史记录 54 | clearHistory:function(e){ 55 | this.setData({ 56 | historyPlaces:[] 57 | }) 58 | wx.removeStorageSync('historySearch'); 59 | }, 60 | 61 | /** 62 | * 生命周期函数--监听页面加载 63 | */ 64 | onLoad: function (options) { 65 | wx.getStorage({ 66 | key: 'historySearch', 67 | success: (res)=>{ 68 | this.setData({ 69 | historyPlaces:res.data 70 | }) 71 | } 72 | }) 73 | }, 74 | 75 | /** 76 | * 生命周期函数--监听页面初次渲染完成 77 | */ 78 | onReady: function () { 79 | 80 | }, 81 | 82 | /** 83 | * 生命周期函数--监听页面显示 84 | */ 85 | onShow: function () { 86 | 87 | }, 88 | 89 | /** 90 | * 生命周期函数--监听页面隐藏 91 | */ 92 | onHide: function () { 93 | 94 | }, 95 | 96 | /** 97 | * 生命周期函数--监听页面卸载 98 | */ 99 | onUnload: function () { 100 | 101 | }, 102 | 103 | /** 104 | * 页面相关事件处理函数--监听用户下拉动作 105 | */ 106 | onPullDownRefresh: function () { 107 | 108 | }, 109 | 110 | /** 111 | * 页面上拉触底事件的处理函数 112 | */ 113 | onReachBottom: function () { 114 | 115 | }, 116 | 117 | /** 118 | * 用户点击右上角分享 119 | */ 120 | onShareAppMessage: function () { 121 | 122 | } 123 | }) -------------------------------------------------------------------------------- /client/pages/search/search.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "搜索目的地", 3 | "backgroundColor": "#fff" 4 | } -------------------------------------------------------------------------------- /client/pages/search/search.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 17 | 18 | 19 | 20 | 21 | 热门搜索 22 | 23 | 24 | {{item}} 25 | 26 | 27 | 28 | 29 | 30 | 历史搜索 31 | 32 | 33 | {{item}} 34 | 35 | 36 | 37 | 清除历史 38 | 39 | 40 | 41 | 42 | 43 | 44 | {{item}} 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /client/pages/search/search.wxss: -------------------------------------------------------------------------------- 1 | /* pages/search/search.wxss */ 2 | .searchBar{ 3 | width: 100%; 4 | height: 100rpx; 5 | background: #f9f9f9; 6 | display: flex; 7 | align-items: center; 8 | padding: 0 30rpx; 9 | } 10 | .searchBar input{ 11 | background-color: #E5E5E5; 12 | border-radius: 10rpx; 13 | flex:1; 14 | height: 60rpx; 15 | padding-left: 10rpx; 16 | } 17 | .searchBar icon{ 18 | margin: 0 0 0 30rpx; 19 | } 20 | 21 | .content{ 22 | width: 100%; 23 | height: 100%; 24 | background: white; 25 | } 26 | .hot{ 27 | padding: 20rpx; 28 | } 29 | .hot .title{ 30 | width: 100%; 31 | height: 80rpx; 32 | line-height: 80rpx; 33 | border-bottom: 1px solid #e0e0e0; 34 | } 35 | .hot .hotPlaces{ 36 | display: flex; 37 | width: 100%; 38 | justify-content: space-between; 39 | flex-wrap: wrap; 40 | } 41 | .hot .hotPlaces view{ 42 | width: 130rpx; 43 | height: 70rpx; 44 | line-height: 70rpx; 45 | text-align: center; 46 | border: 1px solid #e0e0e0; 47 | border-radius: 6rpx; 48 | margin-top: 30rpx; 49 | } 50 | 51 | .history{ 52 | padding: 20rpx; 53 | } 54 | .history .title{ 55 | width: 100%; 56 | height: 80rpx; 57 | line-height: 80rpx; 58 | border-bottom: 1px solid #e0e0e0; 59 | } 60 | .history .historyPlaces{ 61 | width: 100%; 62 | } 63 | .history .historyPlaces view{ 64 | width: 100%; 65 | height: 80rpx; 66 | line-height: 80rpx; 67 | } 68 | .history .clearHistory{ 69 | width: 100%; 70 | padding: 20rpx 0; 71 | text-align: center; 72 | border-top: 1px solid #e0e0e0; 73 | color:#2dbb55; 74 | } 75 | 76 | .suggest{ 77 | width: 100%; 78 | height: 100%; 79 | background: white; 80 | } 81 | .suggest view{ 82 | width: 100%; 83 | height: 80rpx; 84 | line-height: 80rpx; 85 | padding-left: 30rpx; 86 | } -------------------------------------------------------------------------------- /client/utils/util.js: -------------------------------------------------------------------------------- 1 | const formatTime = date => { 2 | const year = date.getFullYear() 3 | const month = date.getMonth() + 1 4 | const day = date.getDate() 5 | const hour = date.getHours() 6 | const minute = date.getMinutes() 7 | const second = date.getSeconds() 8 | 9 | return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':') 10 | } 11 | 12 | const formatNumber = n => { 13 | n = n.toString() 14 | return n[1] ? n : '0' + n 15 | } 16 | 17 | 18 | // 显示繁忙提示 19 | const showBusy = text => wx.showToast({ 20 | title: text, 21 | icon: 'loading', 22 | duration: 10000 23 | }) 24 | 25 | // 显示成功提示 26 | const showSuccess = text => wx.showToast({ 27 | title: text, 28 | icon: 'success' 29 | }) 30 | 31 | // 显示失败提示 32 | const showModel = (title, content) => { 33 | wx.hideToast(); 34 | 35 | wx.showModal({ 36 | title, 37 | content: JSON.stringify(content), 38 | showCancel: false 39 | }) 40 | } 41 | 42 | //获得可用屏幕宽高 43 | const getClientSize = ()=>{ 44 | const w = wx.getSystemInfoSync().windowWidth; 45 | const h = wx.getSystemInfoSync().windowHeight; 46 | return { 47 | width:w, 48 | height:h 49 | } 50 | } 51 | 52 | 53 | 54 | module.exports = { formatTime, showBusy, showSuccess, showModel, getClientSize } 55 | -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/LICENSE: -------------------------------------------------------------------------------- 1 | LICENSE - "MIT License" 2 | 3 | Copyright (c) 2016 by Tencent Cloud 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/README.md: -------------------------------------------------------------------------------- 1 | # 微信小程序客户端腾讯云增强 SDK 2 | 3 | [![Build Status](https://travis-ci.org/tencentyun/wafer-client-sdk.svg?branch=master)](https://travis-ci.org/tencentyun/wafer-client-sdk) 4 | [![Coverage Status](https://coveralls.io/repos/github/tencentyun/wafer-client-sdk/badge.svg?branch=master)](https://coveralls.io/github/tencentyun/wafer-client-sdk?branch=master) 5 | [![License](https://img.shields.io/github/license/tencentyun/wafer-client-sdk.svg)](LICENSE) 6 | 7 | 本 项目是 [Wafer](https://github.com/tencentyun/wafer-solution) 的组成部分,为小程序客户端开发提供 SDK 支持会话服务和信道服务。 8 | 9 | ## SDK 获取与安装 10 | 11 | 解决方案[客户端 Demo](https://github.com/tencentyun/wafer-client-demo) 已经集成并使用最新版的 SDK,需要快速了解的可以从 Demo 开始。 12 | 13 | 如果需要单独开始,本 SDK 已经发布为 bower 模块,可以直接安装到小程序目录中。 14 | 15 | ```sh 16 | npm install -g bower 17 | bower install qcloud-weapp-client-sdk 18 | ``` 19 | 20 | 安装之后,就可以使用 `require` 引用 SDK 模块: 21 | 22 | ```js 23 | var qcloud = require('./bower_components/qcloud-weapp-client-sdk/index.js'); 24 | ``` 25 | 26 | ## 会话服务 27 | 28 | [会话服务](https://github.com/tencentyun/wafer-solution/wiki/%E4%BC%9A%E8%AF%9D%E6%9C%8D%E5%8A%A1)让小程序拥有会话管理能力。 29 | 30 | ### 登录 31 | 32 | 登录可以在小程序和服务器之间建立会话,服务器由此可以获取到用户的标识和信息。 33 | 34 | ```js 35 | var qcloud = require('./bower_components/qcloud-weapp-client-sdk/index.js'); 36 | 37 | // 设置登录地址 38 | qcloud.setLoginUrl('https://199447.qcloud.la/login'); 39 | qcloud.login({ 40 | success: function (userInfo) { 41 | console.log('登录成功', userInfo); 42 | }, 43 | fail: function (err) { 44 | console.log('登录失败', err); 45 | } 46 | }); 47 | ``` 48 | 本 SDK 需要配合云端 SDK 才能提供完整会话服务。通过 [setLoginUrl](#setLoginUrl) 设置登录地址,云服务器在该地址上使用云端 SDK 处理登录请求。 49 | 50 | > `setLoginUrl` 方法设置登录地址之后会一直有效,因此你可以在微信小程序启动时设置。 51 | 52 | 登录成功后,可以获取到当前微信用户的基本信息。 53 | 54 | ### 请求 55 | 56 | 如果希望小程序的网络请求包含会话,登录之后使用 [request](#request) 方法进行网络请求即可。 57 | 58 | ```js 59 | qcloud.request({ 60 | url: 'http://199447.qcloud.la/user', 61 | success: function (response) { 62 | console.log(response); 63 | }, 64 | fail: function (err) { 65 | console.log(err); 66 | } 67 | }); 68 | ``` 69 | 70 | 如果调用 `request` 之前还没有登录,则请求不会带有会话。`request` 方法也支持 `login` 参数支持在请求之前自动登录。 71 | 72 | ```js 73 | // 使用 login 参数之前,需要设置登录地址 74 | qcloud.setLoginUrl('https://199447.qcloud.la/login'); 75 | qcloud.request({ 76 | login: true, 77 | url: 'http://199447.qcloud.la/user', 78 | success: function (response) { 79 | console.log(response); 80 | }, 81 | fail: function (err) { 82 | console.log(err); 83 | } 84 | }); 85 | ``` 86 | 87 | 关于会话服务详细技术说明,请参考 [Wiki](https://github.com/tencentyun/wafer-solution/wiki/%E4%BC%9A%E8%AF%9D%E6%9C%8D%E5%8A%A1)。 88 | 89 | ## 信道服务 90 | 91 | [信道服务](https://github.com/tencentyun/wafer-solution/wiki/%E4%BF%A1%E9%81%93%E6%9C%8D%E5%8A%A1)小程序支持利用腾讯云的信道资源使用 WebSocket 服务。 92 | 93 | ```js 94 | // 创建信道,需要给定后台服务地址 95 | var tunnel = this.tunnel = new qcloud.Tunnel('https://199447.qcloud.la/tunnel'); 96 | 97 | // 监听信道内置消息,包括 connect/close/reconnecting/reconnect/error 98 | tunnel.on('connect', () => console.log('WebSocket 信道已连接')); 99 | tunnel.on('close', () => console.log('WebSocket 信道已断开')); 100 | tunnel.on('reconnecting', () => console.log('WebSocket 信道正在重连...')); 101 | tunnel.on('reconnect', () => console.log('WebSocket 信道重连成功')); 102 | tunnel.on('error', error => console.error('信道发生错误:', error)); 103 | 104 | // 监听自定义消息(服务器进行推送) 105 | tunnel.on('speak', speak => console.log('收到 speak 消息:', speak)); 106 | 107 | // 打开信道 108 | tunnel.open(); 109 | // 发送消息 110 | tunnel.emit('speak', { word: "hello", who: { nickName: "techird" }}); 111 | // 关闭信道 112 | tunnel.close(); 113 | ``` 114 | 115 | 信道服务同样需要业务服务器配合云端 SDK 支持,构造信道实例的时候需要提供业务服务器提供的信道服务地址。通过监听信道消息以及自定义消息来通过信道实现业务。 116 | 117 | 关于信道使用的更完整实例,建议参考客户端 Demo 中的[三木聊天室应用源码](https://github.com/tencentyun/wafer-client-demo/blob/master/pages/chat/chat.js)。 118 | 119 | 关于信道服务详细技术说明,请参考 [Wiki](https://github.com/tencentyun/wafer-solution/wiki/%E4%BF%A1%E9%81%93%E6%9C%8D%E5%8A%A1)。 120 | 121 | ## API 122 | 123 | 124 | ### setLoginUrl 125 | 设置会话服务登录地址。 126 | 127 | #### 语法 128 | ```js 129 | qcloud.setLoginUrl(loginUrl); 130 | ``` 131 | 132 | #### 参数 133 | |参数         |类型 |说明 134 | |-------------|---------------|-------------- 135 | |loginUrl |string |会话服务登录地址 136 | 137 | ### login 138 | 登录,建立微信小程序会话。 139 | 140 | #### 语法 141 | ```js 142 | qcloud.login(options); 143 | ``` 144 | 145 | #### 参数 146 | |参数         |类型 |说明 147 | |-------------|---------------|-------------- 148 | |options     |PlainObject   |会话服务登录地址 149 | |options.success | () => void | 登录成功的回调 150 | |options.error | (error) => void | 登录失败的回调 151 | 152 | 153 | ### request 154 | 进行带会话的请求。 155 | 156 | #### 语法 157 | ```js 158 | qcloud.request(options); 159 | ``` 160 | 161 | #### 参数 162 | |参数         |类型 |说明 163 | |-------------|---------------|-------------- 164 | |options     |PlainObject   | 会话服务登录地址 165 | |options.login | bool         | 是否自动登录以获取会话,默认为 false 166 | |options.url   | string       | 必填,要请求的地址 167 | |options.header | PlainObject | 请求头设置,不允许设置 Referer 168 | |options.method | string     | 请求的方法,默认为 GET 169 | |options.success | (response) => void | 登录成功的回调。 170 | |options.error | (error) => void | 登录失败的回调 171 | |options.complete | () => void | 登录完成后回调,无论成功还是失败 172 | 173 | ### Tunnel 174 | 175 | 表示一个信道。由于小程序的限制,同一时间只能有一个打开的信道。 176 | 177 | #### constructor 178 | 179 | ##### 语法 180 | ```js 181 | var tunnel = new Tunnel(tunnelUrl); 182 | ``` 183 | 184 | #### 参数 185 | |参数         |类型 |说明 186 | |-------------|---------------|-------------- 187 | |tunnelUrl   |String   | 会话服务登录地址 188 | 189 | 190 | #### on 191 | 监听信道上的事件。信道上事件包括系统事件和服务器推送消息。 192 | 193 | ##### 语法 194 | ```js 195 | tunnel.on(type, listener); 196 | ``` 197 | 198 | ##### 参数 199 | |参数         |类型 |说明 200 | |-------------|---------------|-------------- 201 | |type       |string     | 监听的事件类型 202 | |listener     |(message?: any) => void | 监听器,具体类型的事件发生时调用监听器。如果是消息,则会有消息内容。 203 | 204 | ##### 事件 205 | |事件 |说明 206 | |-------------|------------------------------- 207 | |connect |信道连接成功后回调 208 | |close |信道关闭后回调 209 | |reconnecting |信道发生重连时回调 210 | |reconnected |信道重连成功后回调 211 | |error |信道发生错误后回调 212 | |[message]   |信道服务器推送过来的消息类型,如果消息类型和上面内置的时间类型冲突,需要在监听的时候在消息类型前加 `@` 213 | |\*           |监听所有事件和消息,监听器第一个参数接收到时间或消息类型  214 | 215 | #### open 216 | 打开信道,建立连接。由于小程序的限制,同一时间只能有一个打开的信道。 217 | 218 | ##### 语法 219 | ```js 220 | tunnel.open(); 221 | ``` 222 | 223 | #### emit 224 | 向信道推送消息。 225 | 226 | ##### 语法 227 | ```js 228 | tunnel.emit(type, content); 229 | ``` 230 | 231 | ##### 参数 232 | |参数         |类型 |说明 233 | |-------------|---------------|-------------- 234 | |type       |string       | 要推送的消息的类型 235 | |content |any | 要推送的消息的内容 236 | 237 | #### close 238 | 关闭信道 239 | 240 | ##### 语法 241 | ```js 242 | tunnel.close(); 243 | ``` 244 | 245 | ## LICENSE 246 | 247 | [MIT](LICENSE) 248 | -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/index.js: -------------------------------------------------------------------------------- 1 | var constants = require('./lib/constants'); 2 | var login = require('./lib/login'); 3 | var Session = require('./lib/session'); 4 | var request = require('./lib/request'); 5 | var Tunnel = require('./lib/tunnel'); 6 | 7 | var exports = module.exports = { 8 | login: login.login, 9 | setLoginUrl: login.setLoginUrl, 10 | LoginError: login.LoginError, 11 | 12 | clearSession: Session.clear, 13 | 14 | request: request.request, 15 | RequestError: request.RequestError, 16 | 17 | Tunnel: Tunnel, 18 | }; 19 | 20 | // 导出错误类型码 21 | Object.keys(constants).forEach(function (key) { 22 | if (key.indexOf('ERR_') === 0) { 23 | exports[key] = constants[key]; 24 | } 25 | }); -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/lib/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | WX_HEADER_CODE: 'X-WX-Code', 3 | WX_HEADER_ENCRYPTED_DATA: 'X-WX-Encrypted-Data', 4 | WX_HEADER_IV: 'X-WX-IV', 5 | WX_HEADER_ID: 'X-WX-Id', 6 | WX_HEADER_SKEY: 'X-WX-Skey', 7 | 8 | WX_SESSION_MAGIC_ID: 'F2C224D4-2BCE-4C64-AF9F-A6D872000D1A', 9 | 10 | ERR_INVALID_PARAMS: 'ERR_INVALID_PARAMS', 11 | 12 | ERR_WX_LOGIN_FAILED: 'ERR_WX_LOGIN_FAILED', 13 | ERR_WX_GET_USER_INFO: 'ERR_WX_GET_USER_INFO', 14 | ERR_LOGIN_TIMEOUT: 'ERR_LOGIN_TIMEOUT', 15 | ERR_LOGIN_FAILED: 'ERR_LOGIN_FAILED', 16 | ERR_LOGIN_SESSION_NOT_RECEIVED: 'ERR_LOGIN_MISSING_SESSION', 17 | 18 | ERR_SESSION_INVALID: 'ERR_SESSION_INVALID', 19 | ERR_CHECK_LOGIN_FAILED: 'ERR_CHECK_LOGIN_FAILED', 20 | }; -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/lib/login.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils'); 2 | var constants = require('./constants'); 3 | var Session = require('./session'); 4 | 5 | /*** 6 | * @class 7 | * 表示登录过程中发生的异常 8 | */ 9 | var LoginError = (function () { 10 | function LoginError(type, message) { 11 | Error.call(this, message); 12 | this.type = type; 13 | this.message = message; 14 | } 15 | 16 | LoginError.prototype = new Error(); 17 | LoginError.prototype.constructor = LoginError; 18 | 19 | return LoginError; 20 | })(); 21 | 22 | /** 23 | * 微信登录,获取 code 和 encryptData 24 | */ 25 | var getWxLoginResult = function getLoginCode(callback) { 26 | wx.login({ 27 | success: function (loginResult) { 28 | wx.getUserInfo({ 29 | success: function (userResult) { 30 | callback(null, { 31 | code: loginResult.code, 32 | encryptedData: userResult.encryptedData, 33 | iv: userResult.iv, 34 | userInfo: userResult.userInfo, 35 | }); 36 | }, 37 | 38 | fail: function (userError) { 39 | var error = new LoginError(constants.ERR_WX_GET_USER_INFO, '获取微信用户信息失败,请检查网络状态'); 40 | error.detail = userError; 41 | callback(error, null); 42 | }, 43 | }); 44 | }, 45 | 46 | fail: function (loginError) { 47 | var error = new LoginError(constants.ERR_WX_LOGIN_FAILED, '微信登录失败,请检查网络状态'); 48 | error.detail = loginError; 49 | callback(error, null); 50 | }, 51 | }); 52 | }; 53 | 54 | var noop = function noop() {}; 55 | var defaultOptions = { 56 | method: 'GET', 57 | success: noop, 58 | fail: noop, 59 | loginUrl: null, 60 | }; 61 | 62 | /** 63 | * @method 64 | * 进行服务器登录,以获得登录会话 65 | * 66 | * @param {Object} options 登录配置 67 | * @param {string} options.loginUrl 登录使用的 URL,服务器应该在这个 URL 上处理登录请求 68 | * @param {string} [options.method] 请求使用的 HTTP 方法,默认为 "GET" 69 | * @param {Function} options.success(userInfo) 登录成功后的回调函数,参数 userInfo 微信用户信息 70 | * @param {Function} options.fail(error) 登录失败后的回调函数,参数 error 错误信息 71 | */ 72 | var login = function login(options) { 73 | options = utils.extend({}, defaultOptions, options); 74 | 75 | if (!defaultOptions.loginUrl) { 76 | options.fail(new LoginError(constants.ERR_INVALID_PARAMS, '登录错误:缺少登录地址,请通过 setLoginUrl() 方法设置登录地址')); 77 | return; 78 | } 79 | 80 | var doLogin = () => getWxLoginResult(function (wxLoginError, wxLoginResult) { 81 | if (wxLoginError) { 82 | options.fail(wxLoginError); 83 | return; 84 | } 85 | 86 | var userInfo = wxLoginResult.userInfo; 87 | 88 | // 构造请求头,包含 code、encryptedData 和 iv 89 | var code = wxLoginResult.code; 90 | var encryptedData = wxLoginResult.encryptedData; 91 | var iv = wxLoginResult.iv; 92 | var header = {}; 93 | 94 | header[constants.WX_HEADER_CODE] = code; 95 | header[constants.WX_HEADER_ENCRYPTED_DATA] = encryptedData; 96 | header[constants.WX_HEADER_IV] = iv; 97 | 98 | // 请求服务器登录地址,获得会话信息 99 | wx.request({ 100 | url: options.loginUrl, 101 | header: header, 102 | method: options.method, 103 | data: options.data, 104 | success: function (result) { 105 | var data = result.data; 106 | 107 | // 成功地响应会话信息 108 | if (data && data.code === 0 && data.data.skey) { 109 | var res = data.data 110 | if (res.userinfo) { 111 | Session.set(res.skey); 112 | options.success(userInfo); 113 | } else { 114 | var errorMessage = '登录失败(' + data.error + '):' + (data.message || '未知错误'); 115 | var noSessionError = new LoginError(constants.ERR_LOGIN_SESSION_NOT_RECEIVED, errorMessage); 116 | options.fail(noSessionError); 117 | } 118 | 119 | // 没有正确响应会话信息 120 | } else { 121 | var noSessionError = new LoginError(constants.ERR_LOGIN_SESSION_NOT_RECEIVED, JSON.stringify(data)); 122 | options.fail(noSessionError); 123 | } 124 | }, 125 | 126 | // 响应错误 127 | fail: function (loginResponseError) { 128 | var error = new LoginError(constants.ERR_LOGIN_FAILED, '登录失败,可能是网络错误或者服务器发生异常'); 129 | options.fail(error); 130 | }, 131 | }); 132 | }); 133 | 134 | var session = Session.get(); 135 | if (session) { 136 | wx.checkSession({ 137 | success: function () { 138 | options.success(session.userInfo); 139 | }, 140 | 141 | fail: function () { 142 | Session.clear(); 143 | doLogin(); 144 | }, 145 | }); 146 | } else { 147 | doLogin(); 148 | } 149 | }; 150 | 151 | var setLoginUrl = function (loginUrl) { 152 | defaultOptions.loginUrl = loginUrl; 153 | }; 154 | 155 | module.exports = { 156 | LoginError: LoginError, 157 | login: login, 158 | setLoginUrl: setLoginUrl, 159 | }; -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/lib/request.js: -------------------------------------------------------------------------------- 1 | var constants = require('./constants'); 2 | var utils = require('./utils'); 3 | var Session = require('./session'); 4 | var loginLib = require('./login'); 5 | 6 | var noop = function noop() {}; 7 | 8 | var buildAuthHeader = function buildAuthHeader(session) { 9 | var header = {}; 10 | 11 | if (session) { 12 | header[constants.WX_HEADER_SKEY] = session; 13 | } 14 | 15 | return header; 16 | }; 17 | 18 | /*** 19 | * @class 20 | * 表示请求过程中发生的异常 21 | */ 22 | var RequestError = (function () { 23 | function RequestError(type, message) { 24 | Error.call(this, message); 25 | this.type = type; 26 | this.message = message; 27 | } 28 | 29 | RequestError.prototype = new Error(); 30 | RequestError.prototype.constructor = RequestError; 31 | 32 | return RequestError; 33 | })(); 34 | 35 | function request(options) { 36 | if (typeof options !== 'object') { 37 | var message = '请求传参应为 object 类型,但实际传了 ' + (typeof options) + ' 类型'; 38 | throw new RequestError(constants.ERR_INVALID_PARAMS, message); 39 | } 40 | 41 | var requireLogin = options.login; 42 | var success = options.success || noop; 43 | var fail = options.fail || noop; 44 | var complete = options.complete || noop; 45 | var originHeader = options.header || {}; 46 | 47 | // 成功回调 48 | var callSuccess = function () { 49 | success.apply(null, arguments); 50 | complete.apply(null, arguments); 51 | }; 52 | 53 | // 失败回调 54 | var callFail = function (error) { 55 | fail.call(null, error); 56 | complete.call(null, error); 57 | }; 58 | 59 | // 是否已经进行过重试 60 | var hasRetried = false; 61 | 62 | if (requireLogin) { 63 | doRequestWithLogin(); 64 | } else { 65 | doRequest(); 66 | } 67 | 68 | // 登录后再请求 69 | function doRequestWithLogin() { 70 | loginLib.login({ success: doRequest, fail: callFail }); 71 | } 72 | 73 | // 实际进行请求的方法 74 | function doRequest() { 75 | var authHeader = buildAuthHeader(Session.get()); 76 | 77 | wx.request(utils.extend({}, options, { 78 | header: utils.extend({}, originHeader, authHeader), 79 | 80 | success: function (response) { 81 | var data = response.data; 82 | 83 | var error, message; 84 | if (data && data.code === -1) { 85 | Session.clear(); 86 | // 如果是登录态无效,并且还没重试过,会尝试登录后刷新凭据重新请求 87 | if (!hasRetried) { 88 | hasRetried = true; 89 | doRequestWithLogin(); 90 | return; 91 | } 92 | 93 | message = '登录态已过期'; 94 | error = new RequestError(data.error, message); 95 | 96 | callFail(error); 97 | return; 98 | } else { 99 | callSuccess.apply(null, arguments); 100 | } 101 | }, 102 | 103 | fail: callFail, 104 | complete: noop, 105 | })); 106 | }; 107 | 108 | }; 109 | 110 | module.exports = { 111 | RequestError: RequestError, 112 | request: request, 113 | }; -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/lib/session.js: -------------------------------------------------------------------------------- 1 | var constants = require('./constants'); 2 | var SESSION_KEY = 'weapp_session_' + constants.WX_SESSION_MAGIC_ID; 3 | 4 | var Session = { 5 | get: function () { 6 | return wx.getStorageSync(SESSION_KEY) || null; 7 | }, 8 | 9 | set: function (session) { 10 | wx.setStorageSync(SESSION_KEY, session); 11 | }, 12 | 13 | clear: function () { 14 | wx.removeStorageSync(SESSION_KEY); 15 | }, 16 | }; 17 | 18 | module.exports = Session; -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/lib/tunnel.js: -------------------------------------------------------------------------------- 1 | var requestLib = require('./request'); 2 | var wxTunnel = require('./wxTunnel'); 3 | 4 | /** 5 | * 当前打开的信道,同一时间只能有一个信道打开 6 | */ 7 | var currentTunnel = null; 8 | 9 | // 信道状态枚举 10 | var STATUS_CLOSED = Tunnel.STATUS_CLOSED = 'CLOSED'; 11 | var STATUS_CONNECTING = Tunnel.STATUS_CONNECTING = 'CONNECTING'; 12 | var STATUS_ACTIVE = Tunnel.STATUS_ACTIVE = 'ACTIVE'; 13 | var STATUS_RECONNECTING = Tunnel.STATUS_RECONNECTING = 'RECONNECTING'; 14 | 15 | // 错误类型枚举 16 | var ERR_CONNECT_SERVICE = Tunnel.ERR_CONNECT_SERVICE = 1001; 17 | var ERR_CONNECT_SOCKET = Tunnel.ERR_CONNECT_SOCKET = 1002; 18 | var ERR_RECONNECT = Tunnel.ERR_RECONNECT = 2001; 19 | var ERR_SOCKET_ERROR = Tunnel.ERR_SOCKET_ERROR = 3001; 20 | 21 | // 包类型枚举 22 | var PACKET_TYPE_MESSAGE = 'message'; 23 | var PACKET_TYPE_PING = 'ping'; 24 | var PACKET_TYPE_PONG = 'pong'; 25 | var PACKET_TYPE_TIMEOUT = 'timeout'; 26 | var PACKET_TYPE_CLOSE = 'close'; 27 | 28 | // 断线重连最多尝试 5 次 29 | var DEFAULT_MAX_RECONNECT_TRY_TIMES = 5; 30 | 31 | // 每次重连前,等待时间的增量值 32 | var DEFAULT_RECONNECT_TIME_INCREASE = 1000; 33 | 34 | function Tunnel(serviceUrl) { 35 | if (currentTunnel && currentTunnel.status !== STATUS_CLOSED) { 36 | throw new Error('当前有未关闭的信道,请先关闭之前的信道,再打开新信道'); 37 | } 38 | 39 | currentTunnel = this; 40 | 41 | // 等确认微信小程序全面支持 ES6 就不用那么麻烦了 42 | var me = this; 43 | 44 | //========================================================================= 45 | // 暴露实例状态以及方法 46 | //========================================================================= 47 | this.serviceUrl = serviceUrl; 48 | this.socketUrl = null; 49 | this.status = null; 50 | 51 | this.open = openConnect; 52 | this.on = registerEventHandler; 53 | this.emit = emitMessagePacket; 54 | this.close = close; 55 | 56 | this.isClosed = isClosed; 57 | this.isConnecting = isConnecting; 58 | this.isActive = isActive; 59 | this.isReconnecting = isReconnecting; 60 | 61 | 62 | //========================================================================= 63 | // 信道状态处理,状态说明: 64 | // closed - 已关闭 65 | // connecting - 首次连接 66 | // active - 当前信道已经在工作 67 | // reconnecting - 断线重连中 68 | //========================================================================= 69 | function isClosed() { return me.status === STATUS_CLOSED; } 70 | function isConnecting() { return me.status === STATUS_CONNECTING; } 71 | function isActive() { return me.status === STATUS_ACTIVE; } 72 | function isReconnecting() { return me.status === STATUS_RECONNECTING; } 73 | 74 | function setStatus(status) { 75 | var lastStatus = me.status; 76 | if (lastStatus !== status) { 77 | me.status = status; 78 | } 79 | } 80 | 81 | // 初始为关闭状态 82 | setStatus(STATUS_CLOSED); 83 | 84 | 85 | //========================================================================= 86 | // 信道事件处理机制 87 | // 信道事件包括: 88 | // connect - 连接已建立 89 | // close - 连接被关闭(包括主动关闭和被动关闭) 90 | // reconnecting - 开始重连 91 | // reconnect - 重连成功 92 | // error - 发生错误,其中包括连接失败、重连失败、解包失败等等 93 | // [message] - 信道服务器发送过来的其它事件类型,如果事件类型和上面内置的事件类型冲突,将在事件类型前面添加前缀 `@` 94 | //========================================================================= 95 | var preservedEventTypes = 'connect,close,reconnecting,reconnect,error'.split(','); 96 | var eventHandlers = []; 97 | 98 | /** 99 | * 注册消息处理函数 100 | * @param {string} messageType 支持内置消息类型("connect"|"close"|"reconnecting"|"reconnect"|"error")以及业务消息类型 101 | */ 102 | function registerEventHandler(eventType, eventHandler) { 103 | if (typeof eventHandler === 'function') { 104 | eventHandlers.push([eventType, eventHandler]); 105 | } 106 | } 107 | 108 | /** 109 | * 派发事件,通知所有处理函数进行处理 110 | */ 111 | function dispatchEvent(eventType, eventPayload) { 112 | eventHandlers.forEach(function (handler) { 113 | var handleType = handler[0]; 114 | var handleFn = handler[1]; 115 | 116 | if (handleType === '*') { 117 | handleFn(eventType, eventPayload); 118 | } else if (handleType === eventType) { 119 | handleFn(eventPayload); 120 | } 121 | }); 122 | } 123 | 124 | /** 125 | * 派发事件,事件类型和系统保留冲突的,事件名会自动加上 '@' 前缀 126 | */ 127 | function dispatchEscapedEvent(eventType, eventPayload) { 128 | if (preservedEventTypes.indexOf(eventType) > -1) { 129 | eventType = '@' + eventType; 130 | } 131 | 132 | dispatchEvent(eventType, eventPayload); 133 | } 134 | 135 | 136 | //========================================================================= 137 | // 信道连接控制 138 | //========================================================================= 139 | var isFirstConnection = true; 140 | var isOpening = false; 141 | 142 | /** 143 | * 连接信道服务器,获取 WebSocket 连接地址,获取地址成功后,开始进行 WebSocket 连接 144 | */ 145 | function openConnect() { 146 | if (isOpening) return; 147 | isOpening = true; 148 | 149 | // 只有关闭状态才会重新进入准备中 150 | setStatus(isFirstConnection ? STATUS_CONNECTING : STATUS_RECONNECTING); 151 | 152 | requestLib.request({ 153 | url: serviceUrl, 154 | method: 'GET', 155 | success: function (response) { 156 | if (+response.statusCode === 200 && response.data && response.data.data.connectUrl) { 157 | openSocket(me.socketUrl = response.data.data.connectUrl); 158 | } else { 159 | dispatchConnectServiceError(response); 160 | } 161 | }, 162 | fail: dispatchConnectServiceError, 163 | complete: () => isOpening = false, 164 | }); 165 | 166 | function dispatchConnectServiceError(detail) { 167 | if (isFirstConnection) { 168 | setStatus(STATUS_CLOSED); 169 | 170 | dispatchEvent('error', { 171 | code: ERR_CONNECT_SERVICE, 172 | message: '连接信道服务失败,网络错误或者信道服务没有正确响应', 173 | detail: detail || null, 174 | }); 175 | 176 | } else { 177 | startReconnect(detail); 178 | } 179 | } 180 | } 181 | 182 | /** 183 | * 打开 WebSocket 连接,打开后,注册微信的 Socket 处理方法 184 | */ 185 | function openSocket(url) { 186 | wxTunnel.listen({ 187 | onOpen: handleSocketOpen, 188 | onMessage: handleSocketMessage, 189 | onClose: handleSocketClose, 190 | onError: handleSocketError, 191 | }); 192 | 193 | wx.connectSocket({ url: url }); 194 | isFirstConnection = false; 195 | } 196 | 197 | 198 | //========================================================================= 199 | // 处理消息通讯 200 | // 201 | // packet - 数据包,序列化形式为 `${type}` 或者 `${type}:${content}` 202 | // packet.type - 包类型,包括 message, ping, pong, close 203 | // packet.content? - 当包类型为 message 的时候,会附带 message 数据 204 | // 205 | // message - 消息体,会使用 JSON 序列化后作为 packet.content 206 | // message.type - 消息类型,表示业务消息类型 207 | // message.content? - 消息实体,可以为任意类型,表示消息的附带数据,也可以为空 208 | // 209 | // 数据包示例: 210 | // - 'ping' 表示 Ping 数据包 211 | // - 'message:{"type":"speak","content":"hello"}' 表示一个打招呼的数据包 212 | //========================================================================= 213 | 214 | // 连接还没成功建立的时候,需要发送的包会先存放到队列里 215 | var queuedPackets = []; 216 | 217 | /** 218 | * WebSocket 打开之后,更新状态,同时发送所有遗留的数据包 219 | */ 220 | function handleSocketOpen() { 221 | /* istanbul ignore else */ 222 | if (isConnecting()) { 223 | dispatchEvent('connect'); 224 | 225 | } 226 | else if (isReconnecting()) { 227 | dispatchEvent('reconnect'); 228 | resetReconnectionContext(); 229 | } 230 | 231 | setStatus(STATUS_ACTIVE); 232 | emitQueuedPackets(); 233 | nextPing(); 234 | } 235 | 236 | /** 237 | * 收到 WebSocket 数据包,交给处理函数 238 | */ 239 | function handleSocketMessage(message) { 240 | resolvePacket(message.data); 241 | } 242 | 243 | /** 244 | * 发送数据包,如果信道没有激活,将先存放队列 245 | */ 246 | function emitPacket(packet) { 247 | if (isActive()) { 248 | sendPacket(packet); 249 | } else { 250 | queuedPackets.push(packet); 251 | } 252 | } 253 | 254 | /** 255 | * 数据包推送到信道 256 | */ 257 | function sendPacket(packet) { 258 | var encodedPacket = [packet.type]; 259 | 260 | if (packet.content) { 261 | encodedPacket.push(JSON.stringify(packet.content)); 262 | } 263 | 264 | wx.sendSocketMessage({ 265 | data: encodedPacket.join(':'), 266 | fail: handleSocketError, 267 | }); 268 | } 269 | 270 | function emitQueuedPackets() { 271 | queuedPackets.forEach(emitPacket); 272 | 273 | // empty queued packets 274 | queuedPackets.length = 0; 275 | } 276 | 277 | /** 278 | * 发送消息包 279 | */ 280 | function emitMessagePacket(messageType, messageContent) { 281 | var packet = { 282 | type: PACKET_TYPE_MESSAGE, 283 | content: { 284 | type: messageType, 285 | content: messageContent, 286 | }, 287 | }; 288 | 289 | emitPacket(packet); 290 | } 291 | 292 | /** 293 | * 发送 Ping 包 294 | */ 295 | function emitPingPacket() { 296 | emitPacket({ type: PACKET_TYPE_PING }); 297 | } 298 | 299 | /** 300 | * 发送关闭包 301 | */ 302 | function emitClosePacket() { 303 | emitPacket({ type: PACKET_TYPE_CLOSE }); 304 | } 305 | 306 | /** 307 | * 解析并处理从信道接收到的包 308 | */ 309 | function resolvePacket(raw) { 310 | var packetParts = raw.split(':'); 311 | var packetType = packetParts.shift(); 312 | var packetContent = packetParts.join(':') || null; 313 | var packet = { type: packetType }; 314 | 315 | if (packetContent) { 316 | try { 317 | packet.content = JSON.parse(packetContent); 318 | } catch (e) {} 319 | } 320 | 321 | switch (packet.type) { 322 | case PACKET_TYPE_MESSAGE: 323 | handleMessagePacket(packet); 324 | break; 325 | case PACKET_TYPE_PONG: 326 | handlePongPacket(packet); 327 | break; 328 | case PACKET_TYPE_TIMEOUT: 329 | handleTimeoutPacket(packet); 330 | break; 331 | case PACKET_TYPE_CLOSE: 332 | handleClosePacket(packet); 333 | break; 334 | default: 335 | handleUnknownPacket(packet); 336 | break; 337 | } 338 | } 339 | 340 | /** 341 | * 收到消息包,直接 dispatch 给处理函数 342 | */ 343 | function handleMessagePacket(packet) { 344 | var message = packet.content; 345 | dispatchEscapedEvent(message.type, message.content); 346 | } 347 | 348 | 349 | //========================================================================= 350 | // 心跳、断开与重连处理 351 | //========================================================================= 352 | 353 | /** 354 | * Ping-Pong 心跳检测超时控制,这个值有两个作用: 355 | * 1. 表示收到服务器的 Pong 相应之后,过多久再发下一次 Ping 356 | * 2. 如果 Ping 发送之后,超过这个时间还没收到 Pong,断开与服务器的连接 357 | * 该值将在与信道服务器建立连接后被更新 358 | */ 359 | let pingPongTimeout = 15000; 360 | let pingTimer = 0; 361 | let pongTimer = 0; 362 | 363 | /** 364 | * 信道服务器返回 Ping-Pong 控制超时时间 365 | */ 366 | function handleTimeoutPacket(packet) { 367 | var timeout = packet.content * 1000; 368 | /* istanbul ignore else */ 369 | if (!isNaN(timeout)) { 370 | pingPongTimeout = timeout; 371 | ping(); 372 | } 373 | } 374 | 375 | /** 376 | * 收到服务器 Pong 响应,定时发送下一个 Ping 377 | */ 378 | function handlePongPacket(packet) { 379 | nextPing(); 380 | } 381 | 382 | /** 383 | * 发送下一个 Ping 包 384 | */ 385 | function nextPing() { 386 | clearTimeout(pingTimer); 387 | clearTimeout(pongTimer); 388 | pingTimer = setTimeout(ping, pingPongTimeout); 389 | } 390 | 391 | /** 392 | * 发送 Ping,等待 Pong 393 | */ 394 | function ping() { 395 | /* istanbul ignore else */ 396 | if (isActive()) { 397 | emitPingPacket(); 398 | 399 | // 超时没有响应,关闭信道 400 | pongTimer = setTimeout(handlePongTimeout, pingPongTimeout); 401 | } 402 | } 403 | 404 | /** 405 | * Pong 超时没有响应,信道可能已经不可用,需要断开重连 406 | */ 407 | function handlePongTimeout() { 408 | startReconnect('服务器已失去响应'); 409 | } 410 | 411 | // 已经重连失败的次数 412 | var reconnectTryTimes = 0; 413 | 414 | // 最多允许失败次数 415 | var maxReconnectTryTimes = Tunnel.MAX_RECONNECT_TRY_TIMES || DEFAULT_MAX_RECONNECT_TRY_TIMES; 416 | 417 | // 重连前等待的时间 418 | var waitBeforeReconnect = 0; 419 | 420 | // 重连前等待时间增量 421 | var reconnectTimeIncrease = Tunnel.RECONNECT_TIME_INCREASE || DEFAULT_RECONNECT_TIME_INCREASE; 422 | 423 | var reconnectTimer = 0; 424 | 425 | function startReconnect(lastError) { 426 | if (reconnectTryTimes >= maxReconnectTryTimes) { 427 | close(); 428 | 429 | dispatchEvent('error', { 430 | code: ERR_RECONNECT, 431 | message: '重连失败', 432 | detail: lastError, 433 | }); 434 | } 435 | else { 436 | wx.closeSocket(); 437 | waitBeforeReconnect += reconnectTimeIncrease; 438 | setStatus(STATUS_RECONNECTING); 439 | reconnectTimer = setTimeout(doReconnect, waitBeforeReconnect); 440 | } 441 | 442 | if (reconnectTryTimes === 0) { 443 | dispatchEvent('reconnecting'); 444 | } 445 | 446 | reconnectTryTimes += 1; 447 | } 448 | 449 | function doReconnect() { 450 | openConnect(); 451 | } 452 | 453 | function resetReconnectionContext() { 454 | reconnectTryTimes = 0; 455 | waitBeforeReconnect = 0; 456 | } 457 | 458 | /** 459 | * 收到服务器的关闭请求 460 | */ 461 | function handleClosePacket(packet) { 462 | close(); 463 | } 464 | 465 | function handleUnknownPacket(packet) { 466 | // throw away 467 | } 468 | 469 | var isClosing = false; 470 | 471 | /** 472 | * 收到 WebSocket 断开的消息,处理断开逻辑 473 | */ 474 | function handleSocketClose() { 475 | /* istanbul ignore if */ 476 | if (isClosing) return; 477 | 478 | /* istanbul ignore else */ 479 | if (isActive()) { 480 | // 意外断开的情况,进行重连 481 | startReconnect('链接已断开'); 482 | } 483 | } 484 | 485 | function close() { 486 | isClosing = true; 487 | closeSocket(); 488 | setStatus(STATUS_CLOSED); 489 | resetReconnectionContext(); 490 | isFirstConnection = false; 491 | clearTimeout(pingTimer); 492 | clearTimeout(pongTimer); 493 | clearTimeout(reconnectTimer); 494 | dispatchEvent('close'); 495 | isClosing = false; 496 | } 497 | 498 | function closeSocket(emitClose) { 499 | if (isActive() && emitClose !== false) { 500 | emitClosePacket(); 501 | } 502 | 503 | wx.closeSocket(); 504 | } 505 | 506 | 507 | //========================================================================= 508 | // 错误处理 509 | //========================================================================= 510 | 511 | /** 512 | * 错误处理 513 | */ 514 | function handleSocketError(detail) { 515 | switch (me.status) { 516 | case Tunnel.STATUS_CONNECTING: 517 | dispatchEvent('error', { 518 | code: ERR_SOCKET_ERROR, 519 | message: '连接信道失败,网络错误或者信道服务不可用', 520 | detail: detail, 521 | }); 522 | break; 523 | } 524 | } 525 | 526 | } 527 | 528 | module.exports = Tunnel; -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/lib/utils.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 拓展对象 4 | */ 5 | exports.extend = function extend(target) { 6 | var sources = Array.prototype.slice.call(arguments, 1); 7 | 8 | for (var i = 0; i < sources.length; i += 1) { 9 | var source = sources[i]; 10 | for (var key in source) { 11 | if (source.hasOwnProperty(key)) { 12 | target[key] = source[key]; 13 | } 14 | } 15 | } 16 | 17 | return target; 18 | }; -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/lib/wxTunnel.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore next */ 2 | const noop = () => void(0); 3 | 4 | let onOpen, onClose, onMessage, onError; 5 | 6 | /* istanbul ignore next */ 7 | function listen(listener) { 8 | if (listener) { 9 | onOpen = listener.onOpen; 10 | onClose = listener.onClose; 11 | onMessage = listener.onMessage; 12 | onError = listener.onError; 13 | } else { 14 | onOpen = noop; 15 | onClose = noop; 16 | onMessage = noop; 17 | onError = noop; 18 | } 19 | } 20 | 21 | /* istanbul ignore next */ 22 | function bind() { 23 | wx.onSocketOpen(result => onOpen(result)); 24 | wx.onSocketClose(result => onClose(result)); 25 | wx.onSocketMessage(result => onMessage(result)); 26 | wx.onSocketError(error => onError(error)); 27 | } 28 | 29 | listen(null); 30 | bind(); 31 | 32 | module.exports = { listen }; -------------------------------------------------------------------------------- /client/vendor/wafer2-client-sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_from": "wafer2-client-sdk", 3 | "_id": "wafer2-client-sdk@1.0.0", 4 | "_inBundle": false, 5 | "_integrity": "sha1-4hExQwJ+2YIN3LOn0EtbBd8uTYg=", 6 | "_location": "/wafer2-client-sdk", 7 | "_phantomChildren": {}, 8 | "_requested": { 9 | "type": "tag", 10 | "registry": true, 11 | "raw": "wafer2-client-sdk", 12 | "name": "wafer2-client-sdk", 13 | "escapedName": "wafer2-client-sdk", 14 | "rawSpec": "", 15 | "saveSpec": null, 16 | "fetchSpec": "latest" 17 | }, 18 | "_requiredBy": [ 19 | "#USER", 20 | "/" 21 | ], 22 | "_resolved": "http://r.tnpm.oa.com/wafer2-client-sdk/download/wafer2-client-sdk-1.0.0.tgz", 23 | "_shasum": "e2113143027ed9820ddcb3a7d04b5b05df2e4d88", 24 | "_spec": "wafer2-client-sdk", 25 | "_where": "/Users/Jason/Tencent/ide-test/wafer-client-demo", 26 | "author": { 27 | "name": "CFETeam" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/tencentyun/wafer2-client-sdk/issues" 31 | }, 32 | "bundleDependencies": false, 33 | "deprecated": false, 34 | "description": "Wafer client SDK", 35 | "directories": { 36 | "lib": "lib" 37 | }, 38 | "homepage": "https://github.com/tencentyun/wafer2-client-sdk#readme", 39 | "license": "MIT", 40 | "main": "index.js", 41 | "name": "wafer2-client-sdk", 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/tencentyun/wafer2-client-sdk.git" 45 | }, 46 | "version": "1.0.0" 47 | } 48 | -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "client": "./client", 3 | "svr": "./server", 4 | "miniprogramRoot": "./client", 5 | "qcloudRoot": "./server" 6 | } 7 | -------------------------------------------------------------------------------- /screen/find.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmh1996/wx-APP/dbec5001b58621b638eba1cdc814ddc232ac9936/screen/find.gif -------------------------------------------------------------------------------- /screen/index.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmh1996/wx-APP/dbec5001b58621b638eba1cdc814ddc232ac9936/screen/index.gif -------------------------------------------------------------------------------- /screen/me.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmh1996/wx-APP/dbec5001b58621b638eba1cdc814ddc232ac9936/screen/me.gif -------------------------------------------------------------------------------- /screen/place.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmh1996/wx-APP/dbec5001b58621b638eba1cdc814ddc232ac9936/screen/place.gif -------------------------------------------------------------------------------- /screen/search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmh1996/wx-APP/dbec5001b58621b638eba1cdc814ddc232ac9936/screen/search.gif -------------------------------------------------------------------------------- /server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "parserOptions": { 5 | "sourceType": "module" 6 | }, 7 | "extends": "standard", 8 | "rules": { 9 | "indent": [2, 4, { "SwitchCase": 1 }], 10 | "arrow-parens": 0, 11 | "generator-star-spacing": 0 12 | }, 13 | "env": { 14 | "mocha": true 15 | } 16 | } -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # 腾讯云小程序解决方案 Demo - Node.js 2 | 3 | Node.js 版本 Wafer SDK 的服务端 Demo 4 | 5 | ## 下载源码 6 | 7 | 你可以直接通过 git 将代码 clone 到本地,也可以点击[这里](https://github.com/tencentyun/wafer-node-server-demo/releases)下载。 8 | 9 | ```bash 10 | git clone https://github.com/tencentyun/wafer-node-server-demo.git 11 | ``` 12 | 13 | ## 开始使用 14 | 15 | #### 安装依赖 16 | 17 | ```bash 18 | # 安装全局依赖 19 | npm i pm2 nodemon -g 20 | 21 | # 安装项目依赖 22 | npm i 23 | ``` 24 | 25 | #### 启动项目 26 | 27 | ```bash 28 | # 开发环境,监听文件变化自动重启,并会输出 debug 信息 29 | tnpm run dev 30 | 31 | # 线上部署环境 32 | tnpm start 33 | ``` 34 | 35 | 按照[小程序创建资源配置指引](https://github.com/tencentyun/weapp-doc)进行操作,可以得到运行本示例所需的资源和服务,其中包括已部署好的示例代码及自动下发的 SDK 配置文件 `/etc/qcloud/sdk.config`。 36 | 37 | - 示例代码部署目录:`/data/release/node-weapp-demo` 38 | - 运行示例的 Node 版本:`v8.1.0` 39 | - Node 进程管理工具:`pm2` 40 | 41 | ## 项目结构 42 | 43 | ``` 44 | koa-weapp-demo 45 | ├── README.md 46 | ├── app.js 47 | ├── controllers 48 | │ ├── index.js 49 | │ ├── login.js 50 | │ ├── message.js 51 | │ ├── tunnel.js 52 | │ ├── upload.js 53 | │ └── user.js 54 | ├── middlewares 55 | │ └── response.js 56 | ├── config.js 57 | ├── package.json 58 | ├── process.json 59 | ├── nodemon.json 60 | ├── qcloud.js 61 | └── routes 62 | └── index.js 63 | ``` 64 | `app.js` 是 Demo 的主入口文件,Demo 使用 Koa 框架,在 `app.js` 创建一个 Koa 实例并响应请求。 65 | 66 | `routes/index.js` 是 Demo 的路由定义文件 67 | 68 | `controllers` 存放 Demo 所有业务逻辑的目录,`index.js` 不需要修改,他会动态的将 `controllers` 文件夹下的目录结构映射成 modules 的 Object,例如 Demo 中的目录将会被映射成如下的结构: 69 | 70 | ```javascript 71 | // index.js 输出 72 | { 73 | login: require('login'), 74 | message: require('message'), 75 | tunnel: require('tunnel'), 76 | upload: require('upload'), 77 | user: require('user') 78 | } 79 | ``` 80 | 81 | `qcloud.js` 导出了一个 SDK 的单例,包含了所有的 SDK 接口,之后使用的时候只需要 `require` 这个文件就行,无需重复初始化 SDK。 82 | 83 | `config.js` 主要的配置如下: 84 | 85 | ```javascript 86 | { 87 | port: '5757', // 项目启动的端口 88 | 89 | appId: 'wx00dd00dd00dd00dd', // 微信小程序 App ID 90 | appSecret: 'abcdefg', // 微信小程序 App Secret 91 | wxLoginExpires: 7200, // 微信登录态有效期 92 | useQcloudLogin: false, // 是否使用腾讯云代理登录 93 | 94 | /** 95 | * MySQL 配置,用来存储用户登录态和用户信息 96 | * 如果不提供 MySQL 配置,模式会使用自动配置好的本地镜像中的 MySQL 储存信息 97 | * 具体查看文档-登录态储存和校验 98 | **/ 99 | mysql: { 100 | host: 'localhost', 101 | port: 3306, 102 | user: 'root', 103 | db: 'cAuth', 104 | pass: '', 105 | char: 'utf8' 106 | }, 107 | 108 | // COS 配置,用于上传模块使用 109 | cos: { 110 | /** 111 | * 区域 112 | * 华北:cn-north 113 | * 华东:cn-east 114 | * 华南:cn-south 115 | * 西南:cn-southwest 116 | */ 117 | region: 'cn-south', 118 | fileBucket: 'test', // Bucket 名称 119 | uploadFolder: '' // 文件夹 120 | } 121 | } 122 | ``` 123 | 124 | 除了 `config.js` ,腾讯云还会在你初始化小程序解决方案的时候,向你的机器下发 `sdk.config`,里面包含了你的腾讯云 AppId、SecretId、SecretKey 和服务器等信息,无需修改,`qcloud.js` 会自动引入。如果你想要在自己的机器上部署 SDK 的 Demo,请查看[自行部署 Demo 说明]()。 125 | 126 | 除此以外,关于 SDK 的详细配置信息,还可以查看 [SDK 的 API 文档]()。 -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | const app = new Koa() 3 | const debug = require('debug')('koa-weapp-demo') 4 | const response = require('./middlewares/response') 5 | const bodyParser = require('koa-bodyparser') 6 | const config = require('./config') 7 | 8 | // 使用响应处理中间件 9 | app.use(response) 10 | 11 | // 解析请求体 12 | app.use(bodyParser()) 13 | 14 | // 引入路由分发 15 | const router = require('./routes') 16 | app.use(router.routes()) 17 | 18 | // 启动程序,监听端口 19 | app.listen(config.port, () => debug(`listening on port ${config.port}`)) 20 | -------------------------------------------------------------------------------- /server/config.js: -------------------------------------------------------------------------------- 1 | const CONF = { 2 | port: '5757', 3 | rootPathname: '', 4 | 5 | // 微信小程序 App ID 6 | appId: 'wx20f07a50a0944978', 7 | 8 | // 微信小程序 App Secret 9 | appSecret: '7861536042fd56f061968883cb606fa8', 10 | 11 | // 是否使用腾讯云代理登录小程序 12 | useQcloudLogin: true, 13 | 14 | /** 15 | * MySQL 配置,用来存储 session 和用户信息 16 | * 若使用了腾讯云微信小程序解决方案 17 | * 开发环境下,MySQL 的初始密码为您的微信小程序 appid 18 | */ 19 | mysql: { 20 | host: 'localhost', 21 | port: 3306, 22 | user: 'root', 23 | db: 'cAuth', 24 | pass: 'wx20f07a50a0944978', 25 | char: 'utf8mb4' 26 | }, 27 | 28 | cos: { 29 | /** 30 | * 地区简称 31 | * @查看 https://cloud.tencent.com/document/product/436/6224 32 | */ 33 | region: 'ap-guangzhou', 34 | // Bucket 名称 35 | fileBucket: 'qcloudtest', 36 | // 文件夹 37 | uploadFolder: '' 38 | }, 39 | 40 | // 微信登录态有效期 41 | wxLoginExpires: 7200, 42 | wxMessageToken: 'abcdefgh' 43 | } 44 | 45 | module.exports = CONF 46 | -------------------------------------------------------------------------------- /server/controllers/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | const fs = require('fs') 3 | const path = require('path') 4 | 5 | /** 6 | * 映射 d 文件夹下的文件为模块 7 | */ 8 | const mapDir = d => { 9 | const tree = {} 10 | 11 | // 获得当前文件夹下的所有的文件夹和文件 12 | const [dirs, files] = _(fs.readdirSync(d)).partition(p => fs.statSync(path.join(d, p)).isDirectory()) 13 | 14 | // 映射文件夹 15 | dirs.forEach(dir => { 16 | tree[dir] = mapDir(path.join(d, dir)) 17 | }) 18 | 19 | // 映射文件 20 | files.forEach(file => { 21 | if (path.extname(file) === '.js') { 22 | tree[path.basename(file, '.js')] = require(path.join(d, file)) 23 | } 24 | }) 25 | 26 | return tree 27 | } 28 | 29 | // 默认导出当前文件夹下的映射 30 | module.exports = mapDir(path.join(__dirname)) 31 | -------------------------------------------------------------------------------- /server/controllers/login.js: -------------------------------------------------------------------------------- 1 | // 登录授权接口 2 | module.exports = async (ctx, next) => { 3 | // 通过 Koa 中间件进行登录之后 4 | // 登录信息会被存储到 ctx.state.$wxInfo 5 | // 具体查看: 6 | if (ctx.state.$wxInfo.loginState) { 7 | ctx.state.data = ctx.state.$wxInfo.userinfo 8 | ctx.state.data['time'] = Math.floor(Date.now() / 1000) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /server/controllers/message.js: -------------------------------------------------------------------------------- 1 | const { message: { checkSignature } } = require('../qcloud') 2 | 3 | /** 4 | * 响应 GET 请求(响应微信配置时的签名检查请求) 5 | */ 6 | async function get (ctx, next) { 7 | const { signature, timestamp, nonce, echostr } = ctx.query 8 | if (checkSignature(signature, timestamp, nonce)) ctx.body = echostr 9 | else ctx.body = 'ERR_WHEN_CHECK_SIGNATURE' 10 | } 11 | 12 | async function post (ctx, next) { 13 | // 检查签名,确认是微信发出的请求 14 | const { signature, timestamp, nonce } = ctx.query 15 | if (!checkSignature(signature, timestamp, nonce)) ctx.body = 'ERR_WHEN_CHECK_SIGNATURE' 16 | 17 | /** 18 | * 解析微信发送过来的请求体 19 | * 可查看微信文档:https://mp.weixin.qq.com/debug/wxadoc/dev/api/custommsg/receive.html#接收消息和事件 20 | */ 21 | const body = ctx.request.body 22 | 23 | ctx.body = 'success' 24 | } 25 | 26 | module.exports = { 27 | post, 28 | get 29 | } 30 | -------------------------------------------------------------------------------- /server/controllers/travel.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | //获得搜索建议词汇 4 | async function suggest(ctx, next) { 5 | let data; 6 | try { 7 | const res = await axios.get(`https://touch.go.qunar.com/suggest?query=${encodeURIComponent(ctx.query.word)}`); 8 | data = res.data.data.result; 9 | ctx.body = { 10 | code: 200, 11 | data: data 12 | } 13 | } 14 | catch(e){ 15 | ctx.body = { 16 | code: 0, 17 | data: e.toString() 18 | } 19 | } 20 | } 21 | 22 | module.exports = { 23 | suggest 24 | } -------------------------------------------------------------------------------- /server/controllers/tunnel.js: -------------------------------------------------------------------------------- 1 | const { tunnel } = require('../qcloud') 2 | const debug = require('debug')('koa-weapp-demo') 3 | 4 | /** 5 | * 这里实现一个简单的聊天室 6 | * userMap 为 tunnelId 和 用户信息的映射 7 | * 实际使用请使用数据库存储 8 | */ 9 | const userMap = {} 10 | 11 | // 保存 当前已连接的 WebSocket 信道ID列表 12 | const connectedTunnelIds = [] 13 | 14 | /** 15 | * 调用 tunnel.broadcast() 进行广播 16 | * @param {String} type 消息类型 17 | * @param {String} content 消息内容 18 | */ 19 | const $broadcast = (type, content) => { 20 | tunnel.broadcast(connectedTunnelIds, type, content) 21 | .then(result => { 22 | const invalidTunnelIds = result.data && result.data.invalidTunnelIds || [] 23 | 24 | if (invalidTunnelIds.length) { 25 | console.log('检测到无效的信道 IDs =>', invalidTunnelIds) 26 | 27 | // 从 userMap 和 connectedTunnelIds 中将无效的信道记录移除 28 | invalidTunnelIds.forEach(tunnelId => { 29 | delete userMap[tunnelId] 30 | 31 | const index = connectedTunnelIds.indexOf(tunnelId) 32 | if (~index) { 33 | connectedTunnelIds.splice(index, 1) 34 | } 35 | }) 36 | } 37 | }) 38 | } 39 | 40 | /** 41 | * 调用 TunnelService.closeTunnel() 关闭信道 42 | * @param {String} tunnelId 信道ID 43 | */ 44 | const $close = (tunnelId) => { 45 | tunnel.closeTunnel(tunnelId) 46 | } 47 | 48 | /** 49 | * 实现 onConnect 方法 50 | * 在客户端成功连接 WebSocket 信道服务之后会调用该方法, 51 | * 此时通知所有其它在线的用户当前总人数以及刚加入的用户是谁 52 | */ 53 | function onConnect (tunnelId) { 54 | console.log(`[onConnect] =>`, { tunnelId }) 55 | 56 | if (tunnelId in userMap) { 57 | connectedTunnelIds.push(tunnelId) 58 | 59 | $broadcast('people', { 60 | 'total': connectedTunnelIds.length, 61 | 'enter': userMap[tunnelId] 62 | }) 63 | } else { 64 | console.log(`Unknown tunnelId(${tunnelId}) was connectd, close it`) 65 | $close(tunnelId) 66 | } 67 | } 68 | 69 | /** 70 | * 实现 onMessage 方法 71 | * 客户端推送消息到 WebSocket 信道服务器上后,会调用该方法,此时可以处理信道的消息。 72 | * 在本示例,我们处理 `speak` 类型的消息,该消息表示有用户发言。 73 | * 我们把这个发言的信息广播到所有在线的 WebSocket 信道上 74 | */ 75 | function onMessage (tunnelId, type, content) { 76 | console.log(`[onMessage] =>`, { tunnelId, type, content }) 77 | 78 | switch (type) { 79 | case 'speak': 80 | if (tunnelId in userMap) { 81 | $broadcast('speak', { 82 | 'who': userMap[tunnelId], 83 | 'word': content.word 84 | }) 85 | } else { 86 | $close(tunnelId) 87 | } 88 | break 89 | 90 | default: 91 | break 92 | } 93 | } 94 | 95 | /** 96 | * 实现 onClose 方法 97 | * 客户端关闭 WebSocket 信道或者被信道服务器判断为已断开后, 98 | * 会调用该方法,此时可以进行清理及通知操作 99 | */ 100 | function onClose (tunnelId) { 101 | console.log(`[onClose] =>`, { tunnelId }) 102 | 103 | if (!(tunnelId in userMap)) { 104 | console.log(`[onClose][Invalid TunnelId]=>`, tunnelId) 105 | $close(tunnelId) 106 | return 107 | } 108 | 109 | const leaveUser = userMap[tunnelId] 110 | delete userMap[tunnelId] 111 | 112 | const index = connectedTunnelIds.indexOf(tunnelId) 113 | if (~index) { 114 | connectedTunnelIds.splice(index, 1) 115 | } 116 | 117 | // 聊天室没有人了(即无信道ID)不再需要广播消息 118 | if (connectedTunnelIds.length > 0) { 119 | $broadcast('people', { 120 | 'total': connectedTunnelIds.length, 121 | 'leave': leaveUser 122 | }) 123 | } 124 | } 125 | 126 | module.exports = { 127 | // 小程序请求 websocket 地址 128 | get: async ctx => { 129 | const data = await tunnel.getTunnelUrl(ctx.req) 130 | const tunnelInfo = data.tunnel 131 | 132 | userMap[tunnelInfo.tunnelId] = data.userinfo 133 | 134 | ctx.state.data = tunnelInfo 135 | }, 136 | 137 | // 信道将信息传输过来的时候 138 | post: async ctx => { 139 | const packet = await tunnel.onTunnelMessage(ctx.request.body) 140 | 141 | debug('Tunnel recive a package: %o', packet) 142 | 143 | switch (packet.type) { 144 | case 'connect': 145 | onConnect(packet.tunnelId) 146 | break 147 | case 'message': 148 | onMessage(packet.tunnelId, packet.content.messageType, packet.content.messageContent) 149 | break 150 | case 'close': 151 | onClose(packet.tunnelId) 152 | break 153 | } 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /server/controllers/upload.js: -------------------------------------------------------------------------------- 1 | const { uploader } = require('../qcloud') 2 | 3 | module.exports = async ctx => { 4 | // 获取上传之后的结果 5 | // 具体可以查看: 6 | const data = await uploader(ctx.req) 7 | 8 | ctx.state.data = data 9 | } 10 | -------------------------------------------------------------------------------- /server/controllers/user.js: -------------------------------------------------------------------------------- 1 | module.exports = async (ctx, next) => { 2 | // 通过 Koa 中间件进行登录态校验之后 3 | // 登录信息会被存储到 ctx.state.$wxInfo 4 | // 具体查看: 5 | if (ctx.state.$wxInfo.loginState === 1) { 6 | // loginState 为 1,登录态校验成功 7 | ctx.state.data = ctx.state.$wxInfo.userinfo 8 | } else { 9 | ctx.state.code = -1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /server/middlewares/response.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('koa-weapp-demo') 2 | 3 | /** 4 | * 响应处理模块 5 | */ 6 | module.exports = async function (ctx, next) { 7 | try { 8 | // 调用下一个 middleware 9 | await next() 10 | 11 | // 处理响应结果 12 | // 如果直接写入在 body 中,则不作处理 13 | // 如果写在 ctx.body 为空,则使用 state 作为响应 14 | ctx.body = ctx.body ? ctx.body : { 15 | code: ctx.state.code !== undefined ? ctx.state.code : 0, 16 | data: ctx.state.data !== undefined ? ctx.state.data : {} 17 | } 18 | } catch (e) { 19 | // catch 住全局的错误信息 20 | debug('Catch Error: %o', e) 21 | 22 | // 设置状态码为 200 - 服务端错误 23 | ctx.status = 200 24 | 25 | // 输出详细的错误信息 26 | ctx.body = { 27 | code: -1, 28 | error: e && e.message ? e.message : e.toString() 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "restartable": "rs", 3 | "ignore": [ 4 | ".git", 5 | "node_modules/**/node_modules" 6 | ], 7 | "verbose": true, 8 | "execMap": { 9 | "js": "node --harmony" 10 | }, 11 | "env": { 12 | "NODE_ENV": "development", 13 | "DEBUG": "*,-nodemon:*,-nodemon,-knex:pool" 14 | }, 15 | "ext": "js json" 16 | } 17 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-weapp-demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "pm2 start process.prod.json --no-daemon", 8 | "dev": "nodemon --config nodemon.json app.js", 9 | "initdb": "npm install && node tools/initdb.js" 10 | }, 11 | "author": "Jason", 12 | "license": "MIT", 13 | "dependencies": { 14 | "axios": "^0.15.3", 15 | "knex": "^0.13.0", 16 | "koa": "^2.0.0", 17 | "koa-bodyparser": "^3.2.0", 18 | "koa-log4": "^2.1.0", 19 | "koa-router": "^7.0.1", 20 | "lodash": "^4.17.4", 21 | "mkdir-p": "0.0.7", 22 | "mysql": "^2.14.1", 23 | "pify": "^2.3.0", 24 | "wafer-node-sdk": "^1.1.1" 25 | }, 26 | "devDependencies": { 27 | "babel-eslint": "^7.1.0", 28 | "debug": "^2.6.8", 29 | "eslint": "^3.9.1", 30 | "eslint-config-standard": "^6.2.1", 31 | "eslint-plugin-promise": "^3.3.1", 32 | "eslint-plugin-standard": "^2.0.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/process.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "session", 3 | "script": "app.js", 4 | "cwd": "./", 5 | "exec_mode": "fork", 6 | "watch": true, 7 | "ignore_watch": ["tmp"], 8 | "env": { 9 | "NODE_ENV": "production" 10 | }, 11 | "engines": { 12 | "node": ">=7.6" 13 | } 14 | } -------------------------------------------------------------------------------- /server/qcloud.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const qcloud = require('wafer-node-sdk') 3 | 4 | // 获取基础配置 5 | const configs = require('./config') 6 | 7 | // 获取 sdk.config 8 | const sdkConfig = (() => { 9 | const sdkConfigPath = '/data/release/sdk.config.json' 10 | 11 | // 检查文件是否存在 12 | try { 13 | const stats = fs.statSync(sdkConfigPath) 14 | 15 | if (!stats.isFile()) { 16 | console.log('sdk.config.json 不存在,将使用 config.js 中的配置') 17 | return {} 18 | } 19 | } catch (e) { 20 | return {} 21 | } 22 | 23 | // 返回配置信息 24 | try { 25 | const content = fs.readFileSync(sdkConfigPath, 'utf8') 26 | return JSON.parse(content) 27 | } catch (e) { 28 | // 如果配置读取错误或者 JSON 解析错误,则输出空配置项 29 | console.log('sdk.config.json 解析错误,不是 JSON 字符串') 30 | return {} 31 | } 32 | })() 33 | 34 | // 初始化 SDK 35 | // 将基础配置和 sdk.config 合并传入 SDK 并导出初始化完成的 SDK 36 | module.exports = qcloud(Object.assign({}, sdkConfig, configs)) 37 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ajax 服务路由集合 3 | */ 4 | const router = require('koa-router')({ 5 | prefix: '/weapp' 6 | }) 7 | const controllers = require('../controllers') 8 | 9 | // 从 sdk 中取出中间件 10 | // 这里展示如何使用 Koa 中间件完成登录态的颁发与验证 11 | const { auth: { authorizationMiddleware, validationMiddleware } } = require('../qcloud') 12 | 13 | // --- 登录与授权 Demo --- // 14 | // 登录接口 15 | router.get('/login', authorizationMiddleware, controllers.login) 16 | // 用户信息接口(可以用来验证登录态) 17 | router.get('/user', validationMiddleware, controllers.user) 18 | 19 | // --- 图片上传 Demo --- // 20 | // 图片上传接口,小程序端可以直接将 url 填入 wx.uploadFile 中 21 | router.post('/upload', controllers.upload) 22 | 23 | // --- 信道服务接口 Demo --- // 24 | // GET 用来响应请求信道地址的 25 | router.get('/tunnel', controllers.tunnel.get) 26 | // POST 用来处理信道传递过来的消息 27 | router.post('/tunnel', controllers.tunnel.post) 28 | 29 | // --- 客服消息接口 Demo --- // 30 | // GET 用来响应小程序后台配置时发送的验证请求 31 | router.get('/message', controllers.message.get) 32 | // POST 用来处理微信转发过来的客服消息 33 | router.post('/message', controllers.message.post) 34 | 35 | //router.get('/suggest', controllers.travel.suggest) 36 | 37 | module.exports = router 38 | -------------------------------------------------------------------------------- /server/tools.md: -------------------------------------------------------------------------------- 1 | # 腾讯云小程序解决方案 Demo 工具使用文档 2 | 3 | 本文件夹下的脚本为腾讯云小程序解决方案 Demo 配套的工具,旨在让用户方便快捷的使用并创建小程序的开发环境。 4 | 5 | 工具包括: 6 | 7 | - [数据库初始化工具](#数据库初始化工具) 8 | 9 | ## 数据库初始化工具 10 | 11 | 本工具是为了让用户快速的按照腾讯云制定的数据库 schema 创建符合 SDK 标准的数据库结构。 12 | 13 | _**注意**:本工具支持的 MySQL 版本为 **5.7**,并且需提前在数据库中创建名为 `cAuth` 的数据库。`charset` 设置为 `utf8mb4`。_ 14 | 15 | 快速使用: 16 | 17 | ```bash 18 | npm run initdb 19 | ``` 20 | 21 | 或直接执行 `tools` 目录下的 `initdb.js` 文件: 22 | 23 | ```bash 24 | # 请保证已经执行了 npm install 安装了所需要的依赖 25 | node tools/initdb.js 26 | ``` 27 | 28 | 我们提供了初始化的 SQL 文件,你也可以用其他数据库工具(如 Navicat)直接导入 SQL 文件。 29 | -------------------------------------------------------------------------------- /server/tools/cAuth.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : Localhost 5 | Source Server Type : MySQL 6 | Source Server Version : 50717 7 | Source Host : localhost 8 | Source Database : cAuth 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 50717 12 | File Encoding : utf-8 13 | 14 | Date: 08/10/2017 22:22:52 PM 15 | */ 16 | 17 | SET NAMES utf8; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for `cSessionInfo` 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `cSessionInfo`; 24 | CREATE TABLE `cSessionInfo` ( 25 | `open_id` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, 26 | `uuid` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, 27 | `skey` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, 28 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 29 | `last_visit_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 30 | `session_key` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, 31 | `user_info` varchar(2048) COLLATE utf8mb4_unicode_ci NOT NULL, 32 | PRIMARY KEY (`open_id`), 33 | KEY `openid` (`open_id`) USING BTREE, 34 | KEY `skey` (`skey`) USING BTREE 35 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='会话管理用户信息'; 36 | 37 | SET FOREIGN_KEY_CHECKS = 1; 38 | -------------------------------------------------------------------------------- /server/tools/initdb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 腾讯云微信小程序解决方案 3 | * Demo 数据库初始化脚本 4 | * @author Jason 5 | */ 6 | const fs = require('fs') 7 | const path = require('path') 8 | const { mysql: config } = require('../config') 9 | 10 | console.log('\n======================================') 11 | console.log('开始初始化数据库...') 12 | 13 | // 初始化 SQL 文件路径 14 | const INIT_DB_FILE = path.join(__dirname, './cAuth.sql') 15 | 16 | const DB = require('knex')({ 17 | client: 'mysql', 18 | connection: { 19 | host: config.host, 20 | port: config.port, 21 | user: config.user, 22 | password: config.pass, 23 | database: config.db, 24 | charset: config.char, 25 | multipleStatements: true 26 | } 27 | }) 28 | 29 | console.log(`准备读取 SQL 文件:${INIT_DB_FILE}`) 30 | 31 | // 读取 .sql 文件内容 32 | const content = fs.readFileSync(INIT_DB_FILE, 'utf8') 33 | 34 | console.log('开始执行 SQL 文件...') 35 | 36 | // 执行 .sql 文件内容 37 | DB.raw(content).then(res => { 38 | console.log('数据库初始化成功!') 39 | process.exit(0) 40 | }, err => { 41 | throw new Error(err) 42 | }) 43 | --------------------------------------------------------------------------------