├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── app.js ├── app.json ├── app.wxss ├── components └── sponsor │ ├── sponsor.js │ ├── sponsor.json │ ├── sponsor.wxml │ └── sponsor.wxss ├── pages ├── ad │ ├── ad.js │ └── ad.wxml ├── changePlan │ ├── changePlan.js │ ├── changePlan.json │ ├── changePlan.wxml │ └── changePlan.wxss ├── detail │ ├── detail.js │ ├── detail.json │ ├── detail.wxml │ └── detail.wxss ├── feedback │ ├── feedback.js │ ├── feedback.json │ └── feedback.wxml ├── index │ ├── index.js │ ├── index.json │ ├── index.wxml │ └── index.wxss ├── newCard │ ├── newCard.js │ ├── newCard.json │ ├── newCard.wxml │ └── newCard.wxss └── webview │ ├── webview.js │ └── webview.wxml ├── static └── images │ ├── briefcase.svg │ ├── circle-checked.svg │ ├── circle.svg │ ├── cmcom.png │ ├── copper-medal.svg │ ├── gold-medal.svg │ ├── phone.svg │ ├── privilege.svg │ ├── silver-medal.svg │ ├── telecom.svg │ ├── traffic.svg │ └── unicom.svg └── utils └── util.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | project.config.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The GNU General Public License, Version 2, June 1991 (GPLv2) 2 | ============================================================ 3 | 4 | > Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | > 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 6 | 7 | Everyone is permitted to copy and distribute verbatim copies of this license 8 | document, but changing it is not allowed. 9 | 10 | 11 | Preamble 12 | -------- 13 | 14 | The licenses for most software are designed to take away your freedom to share 15 | and change it. By contrast, the GNU General Public License is intended to 16 | guarantee your freedom to share and change free software--to make sure the 17 | software is free for all its users. This General Public License applies to most 18 | of the Free Software Foundation's software and to any other program whose 19 | authors commit to using it. (Some other Free Software Foundation software is 20 | covered by the GNU Lesser General Public License instead.) You can apply it to 21 | your programs, too. 22 | 23 | When we speak of free software, we are referring to freedom, not price. Our 24 | General Public Licenses are designed to make sure that you have the freedom to 25 | distribute copies of free software (and charge for this service if you wish), 26 | that you receive source code or can get it if you want it, that you can change 27 | the software or use pieces of it in new free programs; and that you know you can 28 | do these things. 29 | 30 | To protect your rights, we need to make restrictions that forbid anyone to deny 31 | you these rights or to ask you to surrender the rights. These restrictions 32 | translate to certain responsibilities for you if you distribute copies of the 33 | software, or if you modify it. 34 | 35 | For example, if you distribute copies of such a program, whether gratis or for a 36 | fee, you must give the recipients all the rights that you have. You must make 37 | sure that they, too, receive or can get the source code. And you must show them 38 | these terms so they know their rights. 39 | 40 | We protect your rights with two steps: (1) copyright the software, and (2) offer 41 | you this license which gives you legal permission to copy, distribute and/or 42 | modify the software. 43 | 44 | Also, for each author's protection and ours, we want to make certain that 45 | everyone understands that there is no warranty for this free software. If the 46 | software is modified by someone else and passed on, we want its recipients to 47 | know that what they have is not the original, so that any problems introduced by 48 | others will not reflect on the original authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software patents. We wish 51 | to avoid the danger that redistributors of a free program will individually 52 | obtain patent licenses, in effect making the program proprietary. To prevent 53 | this, we have made it clear that any patent must be licensed for everyone's free 54 | use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and modification 57 | follow. 58 | 59 | 60 | Terms And Conditions For Copying, Distribution And Modification 61 | --------------------------------------------------------------- 62 | 63 | **0.** This License applies to any program or other work which contains a notice 64 | placed by the copyright holder saying it may be distributed under the terms of 65 | this General Public License. The "Program", below, refers to any such program or 66 | work, and a "work based on the Program" means either the Program or any 67 | derivative work under copyright law: that is to say, a work containing the 68 | Program or a portion of it, either verbatim or with modifications and/or 69 | translated into another language. (Hereinafter, translation is included without 70 | limitation in the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not covered by 73 | this License; they are outside its scope. The act of running the Program is not 74 | restricted, and the output from the Program is covered only if its contents 75 | constitute a work based on the Program (independent of having been made by 76 | running the Program). Whether that is true depends on what the Program does. 77 | 78 | **1.** You may copy and distribute verbatim copies of the Program's source code 79 | as you receive it, in any medium, provided that you conspicuously and 80 | appropriately publish on each copy an appropriate copyright notice and 81 | disclaimer of warranty; keep intact all the notices that refer to this License 82 | and to the absence of any warranty; and give any other recipients of the Program 83 | a copy of this License along with the Program. 84 | 85 | You may charge a fee for the physical act of transferring a copy, and you may at 86 | your option offer warranty protection in exchange for a fee. 87 | 88 | **2.** You may modify your copy or copies of the Program or any portion of it, 89 | thus forming a work based on the Program, and copy and distribute such 90 | modifications or work under the terms of Section 1 above, provided that you also 91 | meet all of these conditions: 92 | 93 | * **a)** You must cause the modified files to carry prominent notices stating 94 | that you changed the files and the date of any change. 95 | 96 | * **b)** You must cause any work that you distribute or publish, that in whole 97 | or in part contains or is derived from the Program or any part thereof, to 98 | be licensed as a whole at no charge to all third parties under the terms of 99 | this License. 100 | 101 | * **c)** If the modified program normally reads commands interactively when 102 | run, you must cause it, when started running for such interactive use in the 103 | most ordinary way, to print or display an announcement including an 104 | appropriate copyright notice and a notice that there is no warranty (or 105 | else, saying that you provide a warranty) and that users may redistribute 106 | the program under these conditions, and telling the user how to view a copy 107 | of this License. (Exception: if the Program itself is interactive but does 108 | not normally print such an announcement, your work based on the Program is 109 | not required to print an announcement.) 110 | 111 | These requirements apply to the modified work as a whole. If identifiable 112 | sections of that work are not derived from the Program, and can be reasonably 113 | considered independent and separate works in themselves, then this License, and 114 | its terms, do not apply to those sections when you distribute them as separate 115 | works. But when you distribute the same sections as part of a whole which is a 116 | work based on the Program, the distribution of the whole must be on the terms of 117 | this License, whose permissions for other licensees extend to the entire whole, 118 | and thus to each and every part regardless of who wrote it. 119 | 120 | Thus, it is not the intent of this section to claim rights or contest your 121 | rights to work written entirely by you; rather, the intent is to exercise the 122 | right to control the distribution of derivative or collective works based on the 123 | Program. 124 | 125 | In addition, mere aggregation of another work not based on the Program with the 126 | Program (or with a work based on the Program) on a volume of a storage or 127 | distribution medium does not bring the other work under the scope of this 128 | License. 129 | 130 | **3.** You may copy and distribute the Program (or a work based on it, under 131 | Section 2) in object code or executable form under the terms of Sections 1 and 2 132 | above provided that you also do one of the following: 133 | 134 | * **a)** Accompany it with the complete corresponding machine-readable source 135 | code, which must be distributed under the terms of Sections 1 and 2 above on 136 | a medium customarily used for software interchange; or, 137 | 138 | * **b)** Accompany it with a written offer, valid for at least three years, to 139 | give any third party, for a charge no more than your cost of physically 140 | performing source distribution, a complete machine-readable copy of the 141 | corresponding source code, to be distributed under the terms of Sections 1 142 | and 2 above on a medium customarily used for software interchange; or, 143 | 144 | * **c)** Accompany it with the information you received as to the offer to 145 | distribute corresponding source code. (This alternative is allowed only for 146 | noncommercial distribution and only if you received the program in object 147 | code or executable form with such an offer, in accord with Subsection b 148 | above.) 149 | 150 | The source code for a work means the preferred form of the work for making 151 | modifications to it. For an executable work, complete source code means all the 152 | source code for all modules it contains, plus any associated interface 153 | definition files, plus the scripts used to control compilation and installation 154 | of the executable. However, as a special exception, the source code distributed 155 | need not include anything that is normally distributed (in either source or 156 | binary form) with the major components (compiler, kernel, and so on) of the 157 | operating system on which the executable runs, unless that component itself 158 | accompanies the executable. 159 | 160 | If distribution of executable or object code is made by offering access to copy 161 | from a designated place, then offering equivalent access to copy the source code 162 | from the same place counts as distribution of the source code, even though third 163 | parties are not compelled to copy the source along with the object code. 164 | 165 | **4.** You may not copy, modify, sublicense, or distribute the Program except as 166 | expressly provided under this License. Any attempt otherwise to copy, modify, 167 | sublicense or distribute the Program is void, and will automatically terminate 168 | your rights under this License. However, parties who have received copies, or 169 | rights, from you under this License will not have their licenses terminated so 170 | long as such parties remain in full compliance. 171 | 172 | **5.** You are not required to accept this License, since you have not signed 173 | it. However, nothing else grants you permission to modify or distribute the 174 | Program or its derivative works. These actions are prohibited by law if you do 175 | not accept this License. Therefore, by modifying or distributing the Program (or 176 | any work based on the Program), you indicate your acceptance of this License to 177 | do so, and all its terms and conditions for copying, distributing or modifying 178 | the Program or works based on it. 179 | 180 | **6.** Each time you redistribute the Program (or any work based on the 181 | Program), the recipient automatically receives a license from the original 182 | licensor to copy, distribute or modify the Program subject to these terms and 183 | conditions. You may not impose any further restrictions on the recipients' 184 | exercise of the rights granted herein. You are not responsible for enforcing 185 | compliance by third parties to this License. 186 | 187 | **7.** If, as a consequence of a court judgment or allegation of patent 188 | infringement or for any other reason (not limited to patent issues), conditions 189 | are imposed on you (whether by court order, agreement or otherwise) that 190 | contradict the conditions of this License, they do not excuse you from the 191 | conditions of this License. If you cannot distribute so as to satisfy 192 | simultaneously your obligations under this License and any other pertinent 193 | obligations, then as a consequence you may not distribute the Program at all. 194 | For example, if a patent license would not permit royalty-free redistribution of 195 | the Program by all those who receive copies directly or indirectly through you, 196 | then the only way you could satisfy both it and this License would be to refrain 197 | entirely from distribution of the Program. 198 | 199 | If any portion of this section is held invalid or unenforceable under any 200 | particular circumstance, the balance of the section is intended to apply and the 201 | section as a whole is intended to apply in other circumstances. 202 | 203 | It is not the purpose of this section to induce you to infringe any patents or 204 | other property right claims or to contest validity of any such claims; this 205 | section has the sole purpose of protecting the integrity of the free software 206 | distribution system, which is implemented by public license practices. Many 207 | people have made generous contributions to the wide range of software 208 | distributed through that system in reliance on consistent application of that 209 | system; it is up to the author/donor to decide if he or she is willing to 210 | distribute software through any other system and a licensee cannot impose that 211 | choice. 212 | 213 | This section is intended to make thoroughly clear what is believed to be a 214 | consequence of the rest of this License. 215 | 216 | **8.** If the distribution and/or use of the Program is restricted in certain 217 | countries either by patents or by copyrighted interfaces, the original copyright 218 | holder who places the Program under this License may add an explicit 219 | geographical distribution limitation excluding those countries, so that 220 | distribution is permitted only in or among countries not thus excluded. In such 221 | case, this License incorporates the limitation as if written in the body of this 222 | License. 223 | 224 | **9.** The Free Software Foundation may publish revised and/or new versions of 225 | the General Public License from time to time. Such new versions will be similar 226 | in spirit to the present version, but may differ in detail to address new 227 | problems or concerns. 228 | 229 | Each version is given a distinguishing version number. If the Program specifies 230 | a version number of this License which applies to it and "any later version", 231 | you have the option of following the terms and conditions either of that version 232 | or of any later version published by the Free Software Foundation. If the 233 | Program does not specify a version number of this License, you may choose any 234 | version ever published by the Free Software Foundation. 235 | 236 | **10.** If you wish to incorporate parts of the Program into other free programs 237 | whose distribution conditions are different, write to the author to ask for 238 | permission. For software which is copyrighted by the Free Software Foundation, 239 | write to the Free Software Foundation; we sometimes make exceptions for this. 240 | Our decision will be guided by the two goals of preserving the free status of 241 | all derivatives of our free software and of promoting the sharing and reuse of 242 | software generally. 243 | 244 | 245 | No Warranty 246 | ----------- 247 | 248 | **11.** BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR 249 | THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE 250 | STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM 251 | "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, 252 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 253 | PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 254 | PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 255 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 256 | 257 | **12.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 258 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE 259 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 260 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR 261 | INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA 262 | BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 263 | FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER 264 | OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 套餐助手 4 | 5 | ### 手机套餐对比选购小程序 6 | 7 | 套餐助手是一个帮你在众多互联网套餐中选择最适合的套餐的小程序。 8 | 9 | ## 赞助商 10 | 11 |
12 | 13 | 多态 14 | 15 | 16 | 17 | 18 |
19 | 20 | ## 立即体验 21 | 22 | ![套餐助手小程序码](https://planmaster.prototype.im/minicode.jpg?fix) 23 | 24 | ## 截图 25 | 26 | 27 | 28 | ## 开发使用说明 29 | 30 | 1. 若未安装微信开发中者工具需先安装微信开发者工具。 31 | 2. Clone 项目,使用微信开发者工具新建项目,选择项目目录即可。 32 | 33 | ## 目录结构 34 | 35 | ``` 36 | . 37 | ├── app.js // 在 onLaunch 时做一些统计相关的工作 38 | ├── components 39 | │ └── sponsor // 广告组件,每次 onShow 时显示下一个广告 40 | ├── pages 41 | │ ├── ad // 广告的 webview,广告的打开类型为 webview 时跳到该页面 42 | │ ├── changePlan // 修改套餐页 43 | │ ├── detail // 套餐详情页 44 | │ ├── feedback // 反馈页面 45 | │ ├── index // 首页,费用计算及套餐推荐,推荐套餐按费用升序排列 46 | │ ├── newCard // 办理新卡的页面 47 | │ └── webview // 一个通用的 webview,其他网页都跳到这个页面 48 | ├── static 49 | │ └── images 50 | └── utils 51 | └── util.js // 工具函数,计算套餐的函数放在这里 52 | ``` 53 | ## 项目介绍 54 | 55 | 该小程序可以通过设置每月通话时间、流量等自动计算各种互联网套餐所需的费用并按照升序排列,从而帮助用户选择出最适合自己的套餐。 56 | 57 | 该小程序有很多特色,比如加了很多漂亮的广告,这些广告不仅简洁漂亮,还支持多种打开方式,如跳转到另一个小程序,打开一个网页,甚至往剪贴板中写入一些内容。 58 | 59 | 除此之外,得益于[多态](https://www.duotai.net/?utm_source=planmaster&utm_medium=web&utm_campaign=planmaster-github)提供的表单功能,我们收集到了很多有用的用户反馈,从而帮助我们不断改进完善,也使得我们可以及时纠正套餐信息中存在的错误。 60 | 61 | ## 许可协议 62 | 63 | 套餐助手小程序以附加禁止竞业限制的 GPLv2 许可证开放源代码。 64 | 65 | 本项目的授权协议禁止您使用本项目源码来开发和发布与套餐助手业务相同或相近的小程序(手机套餐对比推荐)。 66 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | App({ 2 | onLaunch: function (opts) { 3 | if (opts.referrerInfo && opts.referrerInfo.extraData && opts.referrerInfo.extraData.from) { 4 | wx.reportAnalytics('adclick', { 5 | from: opts.referrerInfo.extraData.from, 6 | }) 7 | } 8 | wx.setStorage({ 9 | key: 'showTime', 10 | data: null, 11 | }) 12 | }, 13 | getAds: function (cb) { 14 | wx.request({ 15 | url: 'https://planmaster.prototype.im/ads.json', 16 | success: res => { 17 | const { showAd, ads } = res.data 18 | let showTime = Number(wx.getStorageSync('showTime')) || 0 19 | if (showAd) { 20 | wx.setStorage({ 21 | key: 'showTime', 22 | data: showTime + 1 23 | }) 24 | } 25 | const adData = { 26 | showAd: showAd, 27 | ads: ads, 28 | ad: ads[showTime % ads.length], 29 | } 30 | typeof cb === 'function' && cb(adData) 31 | } 32 | }) 33 | }, 34 | }) -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages":[ 3 | "pages/index/index", 4 | "pages/detail/detail", 5 | "pages/newCard/newCard", 6 | "pages/changePlan/changePlan", 7 | "pages/feedback/feedback", 8 | "pages/webview/webview", 9 | "pages/ad/ad" 10 | ], 11 | "window":{ 12 | "backgroundColor": "#F6F6F6", 13 | "backgroundTextStyle":"light", 14 | "navigationBarBackgroundColor": "#F6F6F6", 15 | "navigationBarTitleText": "套餐助手", 16 | "navigationBarTextStyle":"black" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app.wxss: -------------------------------------------------------------------------------- 1 | page { 2 | background: #F6F6F6; 3 | } 4 | button { 5 | margin: 15px; 6 | } 7 | .btn-group { 8 | display: flex; 9 | justify-content: space-between; 10 | width: 100%; 11 | } 12 | .btn-group .btn:first-child { 13 | margin-right: 0; 14 | } 15 | .btn { 16 | flex-grow: 1; 17 | } 18 | .btn[type="primary"] { 19 | color: #FFF; 20 | background: #1ABCFE; 21 | } 22 | .btn[type="default"] { 23 | color: #FFF; 24 | background: #66C53A; 25 | } 26 | .input-value { 27 | display: inline-block; 28 | padding: 0 6px; 29 | border: 1px solid #CCCCCC; 30 | border-radius: 4px; 31 | max-width: 3.5em; 32 | font-size: 16px; 33 | text-align: center; 34 | } 35 | .no-padding { 36 | box-shadow: none !important; 37 | padding: 0 !important; 38 | margin: 20px 0 !important; 39 | } 40 | .footer { 41 | display: flex; 42 | flex-direction: column; 43 | align-items: center; 44 | justify-content: space-between; 45 | padding: 25px; 46 | } 47 | .footer-image image { 48 | height: 32px; 49 | width: 158px; 50 | } 51 | .footer-text { 52 | padding-top: 10px; 53 | font-size: 14px; 54 | color: #A2A2A2; 55 | } -------------------------------------------------------------------------------- /components/sponsor/sponsor.js: -------------------------------------------------------------------------------- 1 | Component({ 2 | properties: { 3 | // 这里定义了innerText属性,属性值可以在组件使用时指定 4 | ad: { 5 | type: Object, 6 | value: {}, 7 | }, 8 | show: { 9 | type: Boolean, 10 | value: false, 11 | } 12 | }, 13 | data: { 14 | }, 15 | methods: { 16 | setSopnsorImageHeight: function (e) { 17 | try { 18 | const { windowWidth } = wx.getSystemInfoSync() 19 | this.setData({ 20 | sopnsorImageHeight: (windowWidth - 30) / 3, 21 | }) 22 | } catch (error) { 23 | console.error(error) 24 | } 25 | }, 26 | goAd: function (e) { 27 | const { ad } = this.data 28 | const { openType } = e.currentTarget.dataset 29 | 30 | if (openType === 'mina') { 31 | wx.navigateToMiniProgram({ 32 | appId: ad.appId, 33 | path: ad.url, 34 | extraData: { 35 | from: 'planmaster', 36 | }, 37 | success(res) { 38 | } 39 | }) 40 | } 41 | if (openType === 'webview') { 42 | wx.navigateTo({ 43 | url: `/pages/ad/ad?url=${ad.url}` 44 | }) 45 | } 46 | if (openType === 'clipboard') { 47 | const { text, modal: { title, content } } = ad.data 48 | wx.setClipboardData({ 49 | data: text, 50 | success: function(res) { 51 | wx.showModal({ 52 | title: title, 53 | content: content, 54 | showCancel: false, 55 | }) 56 | } 57 | }) 58 | } 59 | }, 60 | } 61 | }) -------------------------------------------------------------------------------- /components/sponsor/sponsor.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true 3 | } -------------------------------------------------------------------------------- /components/sponsor/sponsor.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /components/sponsor/sponsor.wxss: -------------------------------------------------------------------------------- 1 | .sponsor { 2 | position: relative; 3 | display: flex; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 15px; 7 | margin: 15px 0; 8 | font-size: 18px; 9 | border-radius: 8px; 10 | background: #FFF; 11 | box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); 12 | } 13 | .sponsor-row { 14 | display: flex; 15 | justify-content: space-between; 16 | align-items: flex-end; 17 | width: 100%; 18 | } 19 | .sponsor-row + .sponsor-row { 20 | margin-top: 10px; 21 | } 22 | .sponsor-column { 23 | display: flex; 24 | flex-direction: column; 25 | justify-content: center; 26 | align-items: space-between; 27 | align-self: stretch; 28 | flex-grow: 1; 29 | } 30 | .sponsor-img-box { 31 | flex-grow: 0; 32 | margin-right: 10px; 33 | } 34 | .sponsor-img { 35 | height: 60px; 36 | width: 50px; 37 | } 38 | .sponsor-title { 39 | font-size: 16px; 40 | } 41 | .sponsor-subtitle { 42 | font-size: 14px; 43 | color: #666666; 44 | } 45 | .sponsor-picture { 46 | width: 100%; 47 | border-radius: 6px; 48 | } 49 | .sponsor-prompt { 50 | min-width: 2em; 51 | padding: 1px 4px; 52 | text-align: center; 53 | font-size: 12px; 54 | color: #999; 55 | border: 1px solid #CCC; 56 | border-radius: 4px; 57 | } -------------------------------------------------------------------------------- /pages/ad/ad.js: -------------------------------------------------------------------------------- 1 | const app = getApp() 2 | 3 | Page({ 4 | data: { 5 | }, 6 | onLoad: function (q) { 7 | this.setData({ 8 | url: q.url, 9 | }) 10 | }, 11 | onShareAppMessage: function (res) { 12 | return { 13 | title: '套餐助手', 14 | path: '/pages/ad/ad?url=' + this.data.url, 15 | } 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /pages/ad/ad.wxml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/changePlan/changePlan.js: -------------------------------------------------------------------------------- 1 | const app = getApp() 2 | const serviceNumbers = { 3 | '中国联通': '10010', 4 | '中国电信': '10000', 5 | '中国移动': '10086', 6 | } 7 | 8 | Page({ 9 | data: { 10 | serviceNumbers: serviceNumbers, 11 | }, 12 | onLoad: function (q) { 13 | this.setData({ 14 | plan: JSON.parse(q.plan), 15 | }) 16 | }, 17 | onShow: function () { 18 | app.getAds(data => this.setData(data)) 19 | }, 20 | goChangePlanNow: function () { 21 | wx.makePhoneCall({ 22 | phoneNumber: serviceNumbers[this.data.plan.operator] 23 | }) 24 | }, 25 | }) 26 | -------------------------------------------------------------------------------- /pages/changePlan/changePlan.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "修改套餐", 3 | "usingComponents": { 4 | "sponsor": "/components/sponsor/sponsor" 5 | } 6 | } -------------------------------------------------------------------------------- /pages/changePlan/changePlan.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 如何将套餐修改为{{plan.cardName}}? 4 | 5 | 要将套餐改为「{{plan.cardName}}」您可直接致电{{serviceNumbers[plan.operator]}},转人工服务,请求客服帮您办理。需要注意的是老用户转套餐将不能享受新用户优惠,但是套餐中包含的免流等特权可以正常享受。 6 | 若客服表示无法办理或需要去营业厅办理,可用请求客服记一个需要回访的投诉,通常不久就会接到回访电话,这时一般就可以成功办理了。 7 | 8 | 9 | 10 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /pages/changePlan/changePlan.wxss: -------------------------------------------------------------------------------- 1 | .block { 2 | margin: 20px; 3 | } 4 | .title { 5 | margin: 15px; 6 | text-align: center; 7 | font-size: 24px; 8 | } 9 | button { 10 | margin: 15px 0; 11 | } -------------------------------------------------------------------------------- /pages/detail/detail.js: -------------------------------------------------------------------------------- 1 | const { computeFee } = require('../../utils/util') 2 | const app = getApp() 3 | 4 | Page({ 5 | data: { 6 | }, 7 | onLoad: function (q) { 8 | let plan = JSON.parse(q.plan) 9 | const call = Number(q.call) 10 | const inProvinceTraffic = Number(q.inProvinceTraffic) 11 | const nationalTraffic = Number(q.nationalTraffic) 12 | 13 | plan.price.costEstimate = computeFee(plan.price, call, inProvinceTraffic, nationalTraffic) 14 | app.getAds(data => this.setData(data)) 15 | this.setData({ 16 | plan: plan, 17 | call: call, 18 | inProvinceTraffic: inProvinceTraffic, 19 | nationalTraffic: nationalTraffic, 20 | }) 21 | }, 22 | onShareAppMessage: function (res) { 23 | const { plan, call, inProvinceTraffic, nationalTraffic } = this.data 24 | 25 | return { 26 | title: plan.partner + plan.cardName, 27 | path: '/pages/detail/detail?plan=' + JSON.stringify(plan) 28 | + '&call=' + call 29 | + '&inProvinceTraffic=' + inProvinceTraffic 30 | + '&nationalTraffic=' + nationalTraffic, 31 | } 32 | }, 33 | computeFee: function () { 34 | let { plan, call, inProvinceTraffic, nationalTraffic } = this.data 35 | plan.price.costEstimate = computeFee(plan.price, call, inProvinceTraffic, nationalTraffic) 36 | this.setData({ 37 | plan: plan, 38 | }) 39 | }, 40 | callInputChange: function (e) { 41 | let call = Math.ceil(e.detail.value.match(/\d+(?:\.\d+)?/)) 42 | this.setData({ 43 | call: call < 0 ? 0 : call 44 | }) 45 | this.computeFee() 46 | }, 47 | inProvinceTrafficInputChange: function (e) { 48 | let traffic = Number(e.detail.value.match(/\d+(?:\.\d+)?/)).toFixed(3) 49 | this.setData({ 50 | inProvinceTraffic: traffic < 0 ? 0 : Number(traffic), 51 | }) 52 | this.computeFee() 53 | }, 54 | nationalTrafficInputChange: function (e) { 55 | let traffic = Number(e.detail.value.match(/\d+(?:\.\d+)?/)).toFixed(3) 56 | this.setData({ 57 | nationalTraffic: traffic < 0 ? 0 : Number(traffic), 58 | }) 59 | this.computeFee() 60 | }, 61 | goChangePlan: function () { 62 | wx.navigateTo({ 63 | url: `/pages/changePlan/changePlan?plan=${JSON.stringify(this.data.plan)}` 64 | }) 65 | }, 66 | goApplyNew: function () { 67 | wx.navigateTo({ 68 | url: `/pages/newCard/newCard?plan=${JSON.stringify(this.data.plan)}` 69 | }) 70 | }, 71 | goFeedback: function () { 72 | wx.navigateTo({ 73 | url: `/pages/feedback/feedback?plan=${JSON.stringify(this.data.plan)}` 74 | }) 75 | }, 76 | goPrototype: function () { 77 | wx.navigateTo({ 78 | url: `/pages/webview/webview?url=https://prototype.im/` 79 | }) 80 | }, 81 | }) 82 | -------------------------------------------------------------------------------- /pages/detail/detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "套餐详情", 3 | "usingComponents": { 4 | "sponsor": "/components/sponsor/sponsor" 5 | } 6 | } -------------------------------------------------------------------------------- /pages/detail/detail.wxml: -------------------------------------------------------------------------------- 1 | 2 | var splitFee = function (fee) { 3 | if (typeof fee === 'string') { 4 | fee = fee.split('.') 5 | } else { 6 | fee = ['00', '00'] 7 | } 8 | return { 9 | feeBig: fee[0], 10 | feeSmall: fee[1] 11 | } 12 | } 13 | module.exports.splitFee = splitFee 14 | 15 | 16 | 17 | 18 | 19 | {{plan.cardName}} 20 | {{plan.price.condition}} 21 | 22 | 23 | 24 | 25 | {{plan.partner}} × {{plan.operator}} 26 | 27 | 28 | 月租{{plan.price.monthlyFee}} 29 | 30 | 31 | 32 | 33 | 套餐内通话 34 | 全国不限量 35 | {{plan.price.callInplan}}分钟 36 | 37 | 38 | 套餐外通话 39 | {{plan.price.callOutPlanPrice}}元每分钟 40 | 41 | 42 | 43 | 套餐内流量 44 | 45 | 46 | 全国不限量 47 | 48 | 49 | 省内不限量 50 | 51 | 52 | 省内{{plan.price.trafficInPlan.inProvince ? plan.price.trafficInPlan.inProvince : '无'}}{{plan.price.trafficInPlan.inProvince ? 'GB' : ''}} 全国{{plan.price.trafficInPlan.national ? plan.price.trafficInPlan.national : '无'}}{{plan.price.trafficInPlan.national ? 'GB' : ''}} 53 | 54 | 55 | 56 | 57 | 套餐外流量 58 | 59 | 60 | 省内 61 | 省内不限量 62 | 省内日租{{plan.price.trafficOutPlanPrice.inProvince.price}}元每天不限量 63 | 省内{{plan.price.trafficOutPlanPrice.inProvince.daily ? '日租' : '月租'}}{{plan.price.trafficOutPlanPrice.inProvince.price}}元每 {{plan.price.trafficOutPlanPrice.inProvince.quantity + ' GB'}} 64 | 全国 65 | 全国不限量 66 | 全国{{plan.price.trafficOutPlanPrice.national.daily ? '日租' : '月租'}}{{plan.price.trafficOutPlanPrice.national.price}}元每 {{plan.price.trafficOutPlanPrice.national.quantity +' GB'}} 67 | 68 | 69 | 70 | 71 | 72 | 73 | 特权 74 | 75 | 76 | 77 | 78 | {{plan.privilege}} 79 | 80 | 81 | 82 | 83 | 费用预估 84 | 请填写您的预计使用量,我可以为您计算预计费用 85 | 86 | 87 | 88 | 通话时间 89 | 90 | 91 | 92 | 93 | 94 | 分钟 95 | 96 | 97 | 98 | 99 | 省内流量 100 | 101 | 102 | 103 | 104 | 105 | GB 106 | 107 | 108 | 109 | 110 | 省外流量 111 | 112 | 113 | 114 | 115 | 116 | GB 117 | 118 | 119 | 120 | 121 | 122 | 预计费用 123 | 124 | 125 | {{m.splitFee(plan.price.costEstimate.totalFee).feeBig}}.{{m.splitFee(plan.price.costEstimate.totalFee).feeSmall}}元 126 | 127 | 128 | 129 | 130 | 月租 131 | 132 | 133 | {{m.splitFee(plan.price.costEstimate.monthlyFee).feeBig}}.{{m.splitFee(plan.price.costEstimate.monthlyFee).feeSmall}}元 134 | 135 | 136 | 137 | 通话 138 | 139 | 140 | {{m.splitFee(plan.price.costEstimate.callFee).feeBig}}.{{m.splitFee(plan.price.costEstimate.callFee).feeSmall}}元 141 | 142 | 143 | 144 | 流量 145 | 146 | 147 | {{m.splitFee(plan.price.costEstimate.trafficFee).feeBig}}.{{m.splitFee(plan.price.costEstimate.trafficFee).feeSmall}}元 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 反馈问题 168 | 169 | 170 | -------------------------------------------------------------------------------- /pages/detail/detail.wxss: -------------------------------------------------------------------------------- 1 | .block { 2 | margin: 18px; 3 | background: #FFF; 4 | font-size: 14px; 5 | box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); 6 | border-radius: 8px; 7 | } 8 | .block-no-style { 9 | background: none; 10 | box-shadow: none; 11 | border-radius: none; 12 | } 13 | .block-row { 14 | display: flex; 15 | justify-content: space-between; 16 | margin: 15px; 17 | padding: 0 10px; 18 | } 19 | .block-row:first-child { 20 | padding-top: 20px; 21 | } 22 | .block-row:last-child { 23 | padding-bottom: 20px; 24 | } 25 | .multi-row { 26 | align-items: flex-start; 27 | } 28 | .text-big { 29 | font-size: 16px; 30 | } 31 | .block-column { 32 | display: flex; 33 | justify-content: space-between; 34 | align-items: center; 35 | line-height: 28px; 36 | vertical-align: middle; 37 | } 38 | .block-divider { 39 | height: 2px; 40 | width: calc(100% - 40px); 41 | margin: 0 20px; 42 | background: rgba(0, 0, 0, 0.04); 43 | border-radius: 20px; 44 | } 45 | .row-icon { 46 | margin-right: 8px; 47 | height: 1em; 48 | width: 1.2em; 49 | } 50 | .row-inline-icon { 51 | margin: 0 8px; 52 | } 53 | .icon-offset { 54 | margin-left: calc(1.2em + 8px); 55 | } 56 | .input-unit { 57 | margin-left: 1em; 58 | color: #666; 59 | } 60 | .title { 61 | margin: 25px 18px; 62 | font-size: 20px; 63 | } 64 | .subtitle { 65 | margin-top: 15px; 66 | font-size: 14px; 67 | color: #666; 68 | } 69 | .block-title { 70 | display: flex; 71 | justify-content: space-between; 72 | align-items: center; 73 | width: 100%; 74 | font-size: 20px; 75 | } 76 | .block-subtitle { 77 | margin-left: 25px; 78 | text-align: right; 79 | font-size: 12px; 80 | color: #FF983C; 81 | } 82 | .block-value { 83 | margin: 0 6px; 84 | font-size: 20px; 85 | } 86 | .align-right { 87 | text-align: right; 88 | } 89 | .sub-fee { 90 | border-top: 1px solid #EEEEEE; 91 | background: #FAFAFA; 92 | border-radius: 8px; 93 | } 94 | .sub-fee .block-row { 95 | padding-top: 0; 96 | } 97 | .monthly-fee { 98 | margin: 0 6px; 99 | font-size: 24px; 100 | color: #29AE60; 101 | vertical-align: baseline; 102 | } 103 | .input-wrapper { 104 | display: flex; 105 | justify-content: space-between; 106 | align-items: center; 107 | width: 8em; 108 | } 109 | .privilege { 110 | font-size: 14px; 111 | color: #27AE60; 112 | } 113 | .fee-value { 114 | margin: 0 5px; 115 | color: #29AE60; 116 | } 117 | .fee-value-big { 118 | font-size: 20px; 119 | } -------------------------------------------------------------------------------- /pages/feedback/feedback.js: -------------------------------------------------------------------------------- 1 | const app = getApp() 2 | 3 | Page({ 4 | data: { 5 | }, 6 | onLoad: function (q) { 7 | try { 8 | let sysInfo = encodeURIComponent(JSON.stringify(wx.getSystemInfoSync())) 9 | let plan = q.plan && JSON.parse(q.plan) 10 | this.setData({ 11 | url: `https://i.duotai.co/forms/dkyqo?${sysInfo ? 'appVersion=' + sysInfo : ''}${plan ? '&plan=' + encodeURIComponent(plan.partner + plan.cardName) : ''}` 12 | }) 13 | } catch (error) { 14 | console.error(error) 15 | } 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /pages/feedback/feedback.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "套餐助手反馈" 3 | } -------------------------------------------------------------------------------- /pages/feedback/feedback.wxml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/index/index.js: -------------------------------------------------------------------------------- 1 | const { computeFee } = require('../../utils/util') 2 | const app = getApp() 3 | let plans 4 | const calls = [0, 50, 100, 200, 300, 500, 700, 1000, 1500, 2000, 2500, 3000] 5 | const traffics = [0, 0.01, 0.02, 0.05, 0.2, 0.5, 0.7, 1, 2, 3, 5, 10, 20, 50, 100, 200] 6 | const shareImage = 'https://planmaster.prototype.im/share_image.png' 7 | let shareImageDownloaded = false 8 | 9 | Page({ 10 | data: { 11 | call: calls[1], 12 | callSlider: 1, 13 | traffic: traffics[7], 14 | trafficSlider: 7, 15 | trafficPerDay: traffics[7] / 30, 16 | freeTraffic: false, 17 | outProvinceDay: 0, 18 | operators: [ 19 | { 20 | show: true, 21 | name: '联通', 22 | checked: true, 23 | img: '/static/images/unicom.svg', 24 | }, 25 | { 26 | show: true, 27 | name: '电信', 28 | checked: false, 29 | img: '/static/images/telecom.svg', 30 | }, 31 | { 32 | show: true, 33 | name: '移动', 34 | checked: false, 35 | img: '/static/images/cmcom.png', 36 | }, 37 | ], 38 | selectedOperators: ['中国联通'], 39 | plans: [ 40 | { 41 | "operator": "中国联通", 42 | "partner": "腾讯", 43 | "cardName": "大王卡", 44 | "guideUrl": "https://planmaster.prototype.im/tencent", 45 | "price": { 46 | "monthlyFee": 19, 47 | "trafficInPlan": { 48 | "national": 0, 49 | "inProvince": 0 50 | }, 51 | "callInplan": 0, 52 | "callOutPlanPrice": 0.1, 53 | "trafficOutPlanPrice": { 54 | "national": { 55 | "daily": true, 56 | "price": 2, 57 | "quantity": 0.5 58 | }, 59 | "inProvince": { 60 | "daily": true, 61 | "price": 1, 62 | "quantity": 0.5 63 | } 64 | } 65 | }, 66 | "privilege": "腾讯视频、王者荣耀等腾讯系应用及游戏免流,斗鱼等直播免流", 67 | "description": "腾讯系应用及游戏免流,熊猫等直播免流" 68 | }, 69 | { 70 | "operator": "中国联通", 71 | "partner": "蚂蚁金服", 72 | "cardName": "大宝卡", 73 | "guideUrl": "https://planmaster.prototype.im/alipay", 74 | "price": { 75 | "monthlyFee": 36, 76 | "condition": "满一年或预存200", 77 | "trafficInPlan": { 78 | "national": 3, 79 | "inProvince": 0 80 | }, 81 | "callInplan": 200, 82 | "callOutPlanPrice": 0.1, 83 | "trafficOutPlanPrice": { 84 | "national": { 85 | "daily": false, 86 | "price": 10, 87 | "quantity": 1 88 | }, 89 | "inProvince": null 90 | } 91 | }, 92 | "privilege": "线下买单送流量。2017年11月起,全系每月+3元可享优酷视频免流(视频广告也免流,缓存下载不免流)", 93 | "description": "送流量,优酷视频免流" 94 | }, 95 | { 96 | "operator": "中国联通", 97 | "partner": "百度", 98 | "cardName": "小圣卡", 99 | "guideUrl": "https://planmaster.prototype.im/sheng", 100 | "price": { 101 | "monthlyFee": 9, 102 | "trafficInPlan": { 103 | "national": 0.1, 104 | "inProvince": 0 105 | }, 106 | "callInplan": 0, 107 | "callOutPlanPrice": 0.1, 108 | "trafficOutPlanPrice": { 109 | "inProvince": { 110 | "daily": true, 111 | "price": 1, 112 | "quantity": 0.8 113 | }, 114 | "national": { 115 | "daily": false, 116 | "price": 15, 117 | "quantity": 1 118 | } 119 | } 120 | }, 121 | "privilege": "手机百度、百度贴吧、百度地图、爱奇艺、好看视频等APP免流量", 122 | "description": "百度系APP免流量" 123 | }, 124 | { 125 | "operator": "中国联通", 126 | "partner": "哔哩哔哩", 127 | "cardName": "33卡", 128 | "guideUrl": "https://planmaster.prototype.im/bilibili", 129 | "price": { 130 | "monthlyFee": 33, 131 | "condition": "满一年或预存200元", 132 | "trafficInPlan": { 133 | "national": 2, 134 | "inProvince": 0 135 | }, 136 | "callInplan": 100, 137 | "callOutPlanPrice": 0.1, 138 | "trafficOutPlanPrice": { 139 | "national": { 140 | "daily": true, 141 | "price": 1, 142 | "quantity": 0.5 143 | }, 144 | "inProvince": null 145 | } 146 | }, 147 | "privilege": "哔哩哔哩免流", 148 | "description": "哔哩哔哩免流" 149 | }, 150 | { 151 | "operator": "中国联通", 152 | "partner": "网易", 153 | "cardName": "大白金卡", 154 | "guideUrl": "https://planmaster.prototype.im/wangyi", 155 | "price": { 156 | "monthlyFee": 19, 157 | "trafficInPlan": { 158 | "national": 1, 159 | "inProvince": 0 160 | }, 161 | "callInplan": 100, 162 | "callOutPlanPrice": 0.1, 163 | "trafficOutPlanPrice": { 164 | "inProvince": { 165 | "daily": true, 166 | "price": 1, 167 | "quantity": 0.8 168 | }, 169 | "national": { 170 | "daily": true, 171 | "price": 2, 172 | "quantity": 0.8 173 | } 174 | } 175 | }, 176 | "privilege": "网易系应用游戏全免流", 177 | "description": "网易系应用游戏全免流" 178 | }, 179 | ] 180 | }, 181 | onShow: function () { 182 | wx.request({ 183 | url: 'https://planmaster.prototype.im', 184 | success: res => { 185 | plans = typeof res.data === 'object' ? res.data : this.data.plans 186 | let { operators } = this.data 187 | 188 | operators = plans.some(p => p.operator === '中国移动') ? operators : operators.filter(o => o.name !== '移动') 189 | this.setData({ 190 | plans: plans, 191 | operators: operators, 192 | }) 193 | this.recommendPlan(plans) 194 | } 195 | }) 196 | app.getAds(data => this.setData(data)) 197 | }, 198 | onShareAppMessage: function (res) { 199 | return { 200 | title: '套餐助手', 201 | path: '/pages/index/index', 202 | } 203 | }, 204 | recommendPlan: function (plans) { 205 | const { call, traffic, outProvinceDay, selectedOperators } = this.data 206 | const inProvinceTraffic = traffic * (30 - outProvinceDay) / 30 207 | const nationalTraffic = traffic * outProvinceDay / 30 208 | let recommendedPlans = plans 209 | .filter(p => selectedOperators.includes(p.operator)) 210 | .map(plan => { 211 | plan.price.costEstimate = computeFee(plan.price, call, inProvinceTraffic, nationalTraffic) 212 | 213 | return plan 214 | }) 215 | .sort((a, b) => a.price.costEstimate.totalFee - b.price.costEstimate.totalFee) 216 | if (selectedOperators.includes('中国联通') && recommendedPlans[0] && recommendedPlans[0].price.costEstimate.totalFee > 150) { 217 | const icecream = recommendedPlans.find(p => p.cardName === '芝麻冰激凌') 218 | recommendedPlans = recommendedPlans.filter(p => p.cardName !== '芝麻冰激凌') 219 | recommendedPlans.unshift(icecream) 220 | } 221 | this.setData({ 222 | recommendedGreatPlans: recommendedPlans.slice(0, 1), 223 | recommendedGoodPlans: recommendedPlans.slice(1, 3), 224 | recommendedOtherPlans: recommendedPlans.slice(3), 225 | }) 226 | }, 227 | callSliderChange: function (e) { 228 | const callSlider = Number(e.detail.value) 229 | this.setData({ 230 | callSlider: callSlider, 231 | call: calls[Number(e.detail.value)], 232 | }) 233 | this.recommendPlan(plans) 234 | }, 235 | callSliderChaning: function (e) { 236 | this.setData({ 237 | call: calls[Number(e.detail.value)], 238 | }) 239 | }, 240 | callInputChange: function (e) { 241 | let call = Math.ceil(e.detail.value.match(/\d+(?:\.\d+)?/)) 242 | this.setData({ 243 | call: call < 0 ? 0 : call 244 | }) 245 | this.recommendPlan(plans) 246 | }, 247 | trafficSliderChange: function (e) { 248 | const trafficSlider = Number(e.detail.value) 249 | this.setData({ 250 | trafficSlider: trafficSlider, 251 | traffic: traffics[Number(e.detail.value)], 252 | trafficPerDay: traffics[Number(e.detail.value)] / 30, 253 | }) 254 | this.recommendPlan(plans) 255 | }, 256 | trafficSliderChanging: function (e) { 257 | this.setData({ 258 | traffic: traffics[Number(e.detail.value)], 259 | }) 260 | }, 261 | trafficInputChange: function (e) { 262 | let traffic = Number(e.detail.value.match(/\d+(?:\.\d+)?/)).toFixed(3) 263 | this.setData({ 264 | traffic: traffic < 0 ? 0 : Number(traffic), 265 | }) 266 | this.recommendPlan(plans) 267 | }, 268 | outProvinceDaySliderChange: function (e) { 269 | this.setData({ 270 | outProvinceDay: Number(e.detail.value), 271 | }) 272 | this.recommendPlan(plans) 273 | }, 274 | outProvinceDaySliderChanging: function (e) { 275 | this.setData({ 276 | outProvinceDay: Number(e.detail.value), 277 | }) 278 | }, 279 | outProvinceDayInputChange: function (e) { 280 | let outProvinceDay = Number(e.detail.value.match(/\d+(?:\.\d+)?/)) 281 | outProvinceDay = outProvinceDay > 30 ? 30 : outProvinceDay < 0 ? 0 : outProvinceDay 282 | this.setData({ 283 | outProvinceDay: outProvinceDay, 284 | }) 285 | this.recommendPlan(plans) 286 | }, 287 | goOutChange: function (e) { 288 | this.setData({ 289 | goOutFrequently: e.detail.value === 'yes', 290 | }) 291 | }, 292 | freeTrafficChange: function (e) { 293 | this.setData({ 294 | freeTraffic: e.detail.value === 'yes', 295 | }) 296 | this.recommendPlan(plans) 297 | }, 298 | applyModeChange: function (e) { 299 | this.setData({ 300 | applyMode: e.detail.value, 301 | }) 302 | this.recommendPlan(plans) 303 | }, 304 | onOperatorChange: function (e) { 305 | const { index, checked } = e.currentTarget.dataset 306 | let { operators, selectedOperators } = this.data 307 | operators[index].checked = !checked 308 | selectedOperators = operators.filter(o => o.checked).map(o => '中国' + o.name) 309 | 310 | this.setData({ 311 | operators: operators, 312 | selectedOperators: selectedOperators, 313 | }) 314 | this.recommendPlan(plans) 315 | }, 316 | previewShareImage: function () { 317 | wx.previewImage({ 318 | current: shareImage, 319 | urls: [shareImage], 320 | }) 321 | }, 322 | getShareImage: function () { 323 | const showSuccessModal = () => { 324 | wx.showModal({ 325 | title: '分享图片保存成功', 326 | content: '分享图片已经保存到相册,请在朋友圈点击选择相册中的图片然后进行分享。', 327 | }) 328 | } 329 | const showFailModal = () => { 330 | wx.showModal({ 331 | title: '保存失败', 332 | content: '请尝试手动保存。', 333 | showCancel: false, 334 | success: () => { 335 | this.previewShareImage() 336 | } 337 | }) 338 | } 339 | const saveImage = () => { 340 | wx.showLoading() 341 | wx.downloadFile({ 342 | header: { 343 | 'Accept': 'image/*' 344 | }, 345 | url: shareImage, 346 | success: res => { 347 | console.log('download success') 348 | wx.hideLoading() 349 | if (res.statusCode !== 200) { 350 | showFailModal() 351 | return 352 | } 353 | wx.saveImageToPhotosAlbum({ 354 | filePath: res.tempFilePath, 355 | success: () => { 356 | shareImageDownloaded = true 357 | showSuccessModal() 358 | }, 359 | fail: res => { 360 | showFailModal() 361 | }, 362 | complete: () => { 363 | } 364 | }) 365 | }, 366 | }) 367 | } 368 | if (shareImageDownloaded) { 369 | showSuccessModal() 370 | return 371 | } 372 | wx.getSetting({ 373 | success: (res) => { 374 | if (res.authSetting["scope.writePhotosAlbum"] === undefined) { 375 | wx.showModal({ 376 | title: '需要图片保存权限', 377 | content: '我们需要保存图片到系统相册的权限,以保存分享图片,请在弹出的授权框中选择允许。', 378 | showCancel: false, 379 | success: () => { 380 | saveImage() 381 | } 382 | }) 383 | } else { 384 | saveImage() 385 | } 386 | } 387 | }) 388 | 389 | }, 390 | goDetail: function (e) { 391 | const { call, traffic, outProvinceDay, recommendedGreatPlans, recommendedGoodPlans, recommendedOtherPlans } = this.data 392 | const inProvinceTraffic = traffic * (30 - outProvinceDay) / 30 393 | const nationalTraffic = traffic * outProvinceDay / 30 394 | const { index, type } = e.currentTarget.dataset 395 | let plans 396 | 397 | switch (type) { 398 | case 'great': 399 | plans =recommendedGreatPlans 400 | break 401 | case 'good': 402 | plans =recommendedGoodPlans 403 | break 404 | case 'other': 405 | plans =recommendedOtherPlans 406 | break 407 | default: 408 | break 409 | } 410 | const plan = plans[index] 411 | if (plan.operator === '中国移动') { 412 | wx.navigateTo({ 413 | url: '/pages/webview/webview?url=' + plan.guideUrl, 414 | }) 415 | } else { 416 | wx.navigateTo({ 417 | url: '/pages/detail/detail?plan=' 418 | + JSON.stringify(plan) 419 | + '&call=' + call 420 | + '&inProvinceTraffic=' + inProvinceTraffic 421 | + '&nationalTraffic=' + nationalTraffic 422 | }) 423 | } 424 | 425 | }, 426 | goFeedback: function () { 427 | wx.navigateTo({ 428 | url: `/pages/feedback/feedback` 429 | }) 430 | }, 431 | goPrototype: function () { 432 | wx.navigateTo({ 433 | url: `/pages/webview/webview?url=https://prototype.im/` 434 | }) 435 | }, 436 | }) 437 | -------------------------------------------------------------------------------- /pages/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": { 3 | "sponsor": "/components/sponsor/sponsor" 4 | } 5 | } -------------------------------------------------------------------------------- /pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | var splitFee = function (fee) { 3 | if (typeof fee === 'string') { 4 | fee = fee.split('.') 5 | } else { 6 | fee = ['00', '00'] 7 | } 8 | return { 9 | feeBig: fee[0], 10 | feeSmall: fee[1] 11 | } 12 | } 13 | module.exports.splitFee = splitFee; 14 | 15 | 16 | 17 | 18 | 使用量设置 19 | 您一般每个月使用多少通话和流量? 20 | 21 | 22 | 23 | 24 | 每月平均通话时间 25 | 分钟 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 每月平均流量使用 37 | GB 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 每月在省外的时间 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 推荐套餐 63 | 以下是我根据您选择的使用量为您推荐的套餐 64 | 65 | 66 | 67 | 68 | {{item.name}} 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | {{item.cardName}} 79 | 80 | {{item.partner}} 81 | 82 | 83 | {{item.price.condition}} 84 | 85 | {{m.splitFee(item.price.costEstimate.totalFee).feeBig}}.{{m.splitFee(item.price.costEstimate.totalFee).feeSmall}}元/月 86 | 87 | {{item.description}} 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | {{item.cardName}} 97 | 98 | {{item.partner}} 99 | 100 | 101 | {{item.price.condition}} 102 | 103 | {{m.splitFee(item.price.costEstimate.totalFee).feeBig}}.{{m.splitFee(item.price.costEstimate.totalFee).feeSmall}}元/月 104 | 105 | {{item.description}} 106 | 107 | 108 | 124 | 125 | 126 | 127 | {{item.cardName}} 128 | 129 | {{item.partner}} 130 | 131 | 132 | {{item.price.condition}} 133 | 134 | {{m.splitFee(item.price.costEstimate.totalFee).feeBig}}.{{m.splitFee(item.price.costEstimate.totalFee).feeSmall}}元/月 135 | 136 | {{item.description}} 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 反馈问题 146 | 147 | 148 | -------------------------------------------------------------------------------- /pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | page { 2 | min-height: 100%; 3 | } 4 | .container { 5 | height: 100%; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | justify-content: space-between; 10 | box-sizing: border-box; 11 | } 12 | .block { 13 | padding: 15px; 14 | width: 100%; 15 | box-sizing: border-box; 16 | } 17 | .block-title { 18 | font-size: 20px; 19 | margin-bottom: 30px; 20 | } 21 | .block-subtitle { 22 | margin: 15px 0; 23 | font-size: 14px; 24 | color: #666; 25 | } 26 | .block-item { 27 | position: relative; 28 | display: flex; 29 | justify-content: space-between; 30 | align-items: center; 31 | margin: 15px 0; 32 | padding: 15px; 33 | font-size: 18px; 34 | border-radius: 8px; 35 | background: #FFF; 36 | box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); 37 | } 38 | .block-item:last-child { 39 | margin-bottom: 0; 40 | } 41 | .block-item.column { 42 | flex-direction: column; 43 | } 44 | .item-row { 45 | display: flex; 46 | justify-content: space-between; 47 | align-items: center; 48 | width: 100%; 49 | } 50 | .item-row + .item-row { 51 | margin-top: 10px; 52 | } 53 | .item-column { 54 | display: flex; 55 | flex-direction: column; 56 | justify-content: space-between; 57 | align-self: stretch; 58 | flex-grow: 1; 59 | } 60 | .item-title { 61 | font-size: 16px; 62 | } 63 | .item-title-icon { 64 | height: 1.2em; 65 | width: 1.2em; 66 | margin-right: 10px; 67 | vertical-align: middle; 68 | } 69 | .checkbox-group { 70 | display: flex; 71 | flex-direction: column; 72 | } 73 | .checkbox { 74 | margin: 10px; 75 | } 76 | .radio-group { 77 | display: flex; 78 | justify-content: space-around; 79 | width: 70%; 80 | margin: 15px auto; 81 | } 82 | .slider-box { 83 | flex-grow: 1; 84 | } 85 | .slider { 86 | margin-left: 10px; 87 | margin-right: 20px; 88 | flex-grow: 1; 89 | } 90 | .slider-value { 91 | display: flex; 92 | justify-content: space-around; 93 | align-items: center; 94 | margin: 15px auto; 95 | width: 70%; 96 | } 97 | .input-unit { 98 | text-align: right; 99 | font-size: 14px; 100 | color: #999; 101 | } 102 | .operator { 103 | display: flex; 104 | justify-content: space-between; 105 | } 106 | .operator-item { 107 | display: flex; 108 | justify-content: space-between; 109 | align-items: center; 110 | flex-grow: 1; 111 | margin-bottom: 15px; 112 | padding: 8px; 113 | font-size: 12px; 114 | line-height: 16px; 115 | border-radius: 8px; 116 | background: #FFF; 117 | box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); 118 | } 119 | 120 | .operator-item + .operator-item { 121 | margin-left: 10px; 122 | } 123 | .operator-text { 124 | vertical-align: middle; 125 | } 126 | .operator-icon { 127 | height: 1em; 128 | width: 1.2em; 129 | vertical-align: middle; 130 | } 131 | .operator-icon-inline { 132 | margin-right: 0.4em; 133 | } 134 | .operator-icon-big { 135 | height: 1.5em; 136 | width: 1.5em; 137 | } 138 | @media (min-device-width: 375px) { 139 | .operator-item { 140 | display: flex; 141 | justify-content: space-between; 142 | align-items: center; 143 | flex-grow: 1; 144 | margin-bottom: 15px; 145 | padding: 10px; 146 | font-size: 14px; 147 | line-height: 16px; 148 | border-radius: 8px; 149 | background: #FFF; 150 | box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); 151 | } 152 | .operator-item + .operator-item { 153 | margin-left: 15px; 154 | } 155 | } 156 | .plan-rank-icon { 157 | margin-right: 10px; 158 | height: 1.5em; 159 | width: 1.2em; 160 | vertical-align: middle; 161 | } 162 | .plan-name text { 163 | line-height: 1.5em; 164 | vertical-align: middle; 165 | } 166 | .plan-fee { 167 | font-size: 14px; 168 | text-align: right; 169 | } 170 | .normal-fee { 171 | margin-bottom: 5px; 172 | } 173 | .condition { 174 | max-width: 15em; 175 | font-size: 12px; 176 | color: #FF983C; 177 | } 178 | .discounted-fee { 179 | max-width: 15em; 180 | font-size: 12px; 181 | color: #60D390; 182 | } 183 | .fee-value { 184 | margin: 0 5px; 185 | color: #FF983C; 186 | } 187 | .fee-value-big { 188 | font-size: 20px; 189 | } 190 | .fee-value.recommanded { 191 | color: #29AE60; 192 | } 193 | .plan-partner-name { 194 | margin-top: 10px; 195 | font-size: 12px; 196 | color: #666; 197 | } 198 | .share-btn { 199 | margin: 0; 200 | margin-top: 10px; 201 | width: calc(50% - 15px); 202 | line-height: 2.4em; 203 | font-size: 16px; 204 | } 205 | .share-btn:not(:first-child) { 206 | margin-left: 15px; 207 | } -------------------------------------------------------------------------------- /pages/newCard/newCard.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const app = getApp() 4 | 5 | Page({ 6 | data: { 7 | }, 8 | 9 | onLoad: function (q) { 10 | this.setData({ 11 | plan: JSON.parse(q.plan), 12 | }) 13 | }, 14 | 15 | }) 16 | -------------------------------------------------------------------------------- /pages/newCard/newCard.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "办理新卡" 3 | } -------------------------------------------------------------------------------- /pages/newCard/newCard.wxml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /pages/newCard/newCard.wxss: -------------------------------------------------------------------------------- 1 | .title { 2 | text-align: center; 3 | font-size: 24px; 4 | } -------------------------------------------------------------------------------- /pages/webview/webview.js: -------------------------------------------------------------------------------- 1 | const app = getApp() 2 | 3 | Page({ 4 | data: { 5 | }, 6 | onLoad: function (q) { 7 | this.setData({ 8 | url: q.url, 9 | }) 10 | }, 11 | onShareAppMessage: function (res) { 12 | return { 13 | title: '套餐助手', 14 | path: '/pages/webview/webview?url=' + this.data.url, 15 | } 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /pages/webview/webview.wxml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/images/briefcase.svg: -------------------------------------------------------------------------------- 1 | 2 | briefcase 3 | Created using Figma 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /static/images/circle-checked.svg: -------------------------------------------------------------------------------- 1 | 2 | Group 2 3 | Created using Figma 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /static/images/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | Ellipse 3 | Created using Figma 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /static/images/cmcom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrototypeFunction/PlanMaster/ffd516cb142153a44fd34162433976aac1fe319a/static/images/cmcom.png -------------------------------------------------------------------------------- /static/images/copper-medal.svg: -------------------------------------------------------------------------------- 1 | 2 | medal 3 | Created using Figma 4 | 5 | 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 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /static/images/gold-medal.svg: -------------------------------------------------------------------------------- 1 | 2 | medal 3 | Created using Figma 4 | 5 | 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 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /static/images/phone.svg: -------------------------------------------------------------------------------- 1 | 2 | phone 3 | Created using Figma 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /static/images/privilege.svg: -------------------------------------------------------------------------------- 1 | 2 | Vector 3 | Created using Figma 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /static/images/silver-medal.svg: -------------------------------------------------------------------------------- 1 | 2 | medal 3 | Created using Figma 4 | 5 | 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 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /static/images/telecom.svg: -------------------------------------------------------------------------------- 1 | 2 | Union 3 | Created using Figma 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /static/images/traffic.svg: -------------------------------------------------------------------------------- 1 | 2 | Group 2 3 | Created using Figma 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /static/images/unicom.svg: -------------------------------------------------------------------------------- 1 | 2 | path9 3 | Created using Figma 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /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 | const computeFee = (price, call, inProvinceTraffic, nationalTraffic) => { 18 | const overCall = call - price.callInplan <= 0 ? 0 : call - price.callInplan 19 | 20 | let trafficPerDay = (inProvinceTraffic + nationalTraffic) / 30 21 | let noInProvinceTrafficDays = 0 22 | let noNationalTrafficDays = 0 23 | let noTrafficDays = 30 - (price.trafficInPlan.inProvince + price.trafficInPlan.national) / trafficPerDay 24 | noTrafficDays = noTrafficDays >= 0 ? noTrafficDays : 0 25 | if (trafficPerDay > 0) { 26 | noInProvinceTrafficDays = noTrafficDays * (inProvinceTraffic / (inProvinceTraffic + nationalTraffic)) > 0 ? noTrafficDays * (inProvinceTraffic / (inProvinceTraffic + nationalTraffic)) : 0 27 | noNationalTrafficDays = noTrafficDays * (nationalTraffic / (inProvinceTraffic + nationalTraffic)) > 0 ? noTrafficDays * (nationalTraffic / (inProvinceTraffic + nationalTraffic)) : 0 28 | } 29 | 30 | if (price.trafficInPlan.inProvince === 0) { 31 | inProvinceTraffic = inProvinceTraffic - price.trafficInPlan.national * inProvinceTraffic / (inProvinceTraffic + nationalTraffic) 32 | nationalTraffic = nationalTraffic - price.trafficInPlan.national * nationalTraffic / (inProvinceTraffic + nationalTraffic) 33 | } else { 34 | inProvinceTraffic = inProvinceTraffic - price.trafficInPlan.inProvince 35 | nationalTraffic = nationalTraffic - price.trafficInPlan.national 36 | } 37 | inProvinceTraffic = inProvinceTraffic >= 0 ? inProvinceTraffic : 0 38 | nationalTraffic = nationalTraffic >= 0 ? nationalTraffic : 0 39 | 40 | if (!price.trafficOutPlanPrice.inProvince) { 41 | nationalTraffic = inProvinceTraffic + nationalTraffic 42 | noNationalTrafficDays = noNationalTrafficDays + noInProvinceTrafficDays 43 | inProvinceTraffic = 0 44 | noInProvinceTrafficDays = 0 45 | } 46 | 47 | let fee = price.monthlyFee 48 | let callFee = overCall * price.callOutPlanPrice 49 | let trafficFee = 0 50 | let inProvinceFee = 0 51 | let nationalFee = 0 52 | const { inProvince, national, daily } = price.trafficOutPlanPrice 53 | 54 | if (inProvinceTraffic + nationalTraffic > 0) { 55 | if (inProvince && national) { 56 | if (inProvince.daily) { 57 | inProvinceFee = Math.ceil(trafficPerDay / inProvince.quantity) * inProvince.price * noInProvinceTrafficDays 58 | } else { 59 | inProvinceFee = Math.ceil(inProvinceTraffic / inProvince.quantity) * inProvince.price 60 | } 61 | if (national.daily) { 62 | nationalFee = Math.ceil(trafficPerDay / national.quantity) * national.price * noNationalTrafficDays 63 | } else { 64 | nationalFee = Math.ceil(nationalTraffic / national.quantity) * national.price 65 | } 66 | } else if (national) { 67 | if (national.daily) { 68 | nationalFee = Math.ceil(trafficPerDay / national.quantity) * national.price * noNationalTrafficDays 69 | } else { 70 | nationalFee = Math.ceil(nationalTraffic / national.quantity) * national.price 71 | } 72 | } 73 | } 74 | 75 | trafficFee = nationalFee + inProvinceFee 76 | 77 | return { 78 | totalFee: (fee + callFee + trafficFee).toFixed(2), 79 | monthlyFee: price.monthlyFee.toFixed(2), 80 | callFee: callFee.toFixed(2), 81 | inProvinceFee: inProvinceFee.toFixed(2), 82 | nationalFee: nationalFee.toFixed(2), 83 | trafficFee: trafficFee.toFixed(2), 84 | } 85 | } 86 | 87 | module.exports = { 88 | formatTime: formatTime, 89 | computeFee: computeFee, 90 | } 91 | --------------------------------------------------------------------------------