├── .editorconfig ├── .env.development ├── .env.production ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public └── index.html ├── src ├── App.vue ├── components │ ├── FollowModal.vue │ └── SearchBar.vue ├── js │ ├── cookie.js │ ├── request.js │ ├── wxAuth.js │ └── wxShare.js ├── main.js ├── pages │ ├── Article.vue │ ├── Home.vue │ ├── NotFound.vue │ ├── QuestionCategory.vue │ ├── QuestionSearch.vue │ ├── Subscribe.vue │ └── WxLogin.vue ├── router.js └── style │ ├── article.css │ └── common.css └── vue.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = false 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 只有以 VUE_APP_ 开头的变量会被 webpack.DefinePlugin 静态嵌入到客户端侧的包中 2 | VUE_APP_WX_APPID=wx7b6ca33b71fbbf27 3 | VUE_APP_WX_QRCODE= -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 只有以 VUE_APP_ 开头的变量会被 webpack.DefinePlugin 静态嵌入到客户端侧的包中 2 | VUE_APP_WX_APPID= 3 | VUE_APP_WX_QRCODE= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wx-client 2 | 微信公众号接口[wx-api](https://github.com/niefy/wx-api)前端项目,包含微信登录、微信分享、CMS文章示例功能 3 | 4 | ## 开发环境 5 | - node.js 6 | 7 | ## 启动步骤 8 | 1. 启动[wx-api](https://github.com/niefy/wx-api)项目,具体流程参考对应项目介绍 9 | 2. 安装依赖 10 | 11 | ``` bash 12 | npm install 13 | ``` 14 | 3. 修改.env.development开发环境配置文件 15 | 4. 编译构建 16 | ``` bash 17 | # 开发环境 18 | npm run serve 19 | 20 | # 生产环境 21 | npm run build 22 | ``` 23 | 5. 浏览器打开如下地址: 24 | - 首页:http://localhost:8080 25 | 26 | ## 体验微信分享、微信授权登录等功能 27 | 由于微信分享和微信登录功能需在正式域名下使用,有如下两种方式体验 28 | ### 方式一:部署到生产环境再看 29 | 生产环境建议打包后将文件部署到nginx,后端请求使用nginx转发功能 30 | nginx.conf配置 31 | ``` 32 | location /wx/ { #转发后端接口 33 | proxy_pass http://127.0.0.1:8088/wx/; 34 | } 35 | ``` 36 | ### 方式二:使用ngrok穿透到内网 37 | 然后配置ngrok映射到nginx端口:http://xxx.ngrok.io -> http://localhost:8080 38 | ``` 39 | ngrok http 8080 40 | ``` 41 | 42 | ### 不管哪种方式都需要检查如下配置: 43 | - 公众号/测试号后台:接口配置是否成功 44 | - 公众号/测试号后台:JS接口安全域名,只填域名,不要带协议及URL,如:xxx.ngrok.io 45 | - 公众号/测试号后台:网页授权回调域名,只填域名,不要带协议及URL,如:xxx.ngrok.io 46 | 47 | 然后可以微信访问开发环境页面或者使用微信开发者工具打开 48 | - 文章(微信分享)示例:http://xxx.ngrok.io/client/article/1 49 | - 微信登录示例:http://xxx.ngrok.io/client/wxLogin 50 | 51 | 52 | ## 截图 53 | ![首页](https://s1.ax1x.com/2020/06/04/tBI5dA.jpg) 54 | ![订阅](https://s1.ax1x.com/2020/06/04/tBIfqH.jpg) 55 | ![关注](https://s1.ax1x.com/2020/06/04/tBIWse.jpg) 56 | ![文章](https://s1.ax1x.com/2020/06/04/tBI4Zd.jpg) 57 | 58 | ## 生产环境如何部署(需先部署wx-api) 59 | 1. 配置wx-client目录下.env.production文件 60 | 2. 编译打包:npm run build 61 | 3. 将打包后的dist目录内文件上传到服务器nginx的资源目录(默认目录名是html) 62 | 4. 修改nginx.conf配置,将/wx/ 请求转发到wx-api生产环境接口地址 63 | 5. 域名映射到nginx所在服务器,前往微信公众号后台配置做开发配置 -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weixin-client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "uat": "vue-cli-service build --mode uat" 9 | }, 10 | "devDependencies": { 11 | "@vue/cli-plugin-babel": "^5.0.8", 12 | "@vue/cli-service": "^5.0.8", 13 | "vue-template-compiler": "^2.6.12" 14 | }, 15 | "postcss": { 16 | "plugins": { 17 | "autoprefixer": {} 18 | } 19 | }, 20 | "browserslist": [ 21 | "> 1%", 22 | "last 2 versions", 23 | "not ie <= 8" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 微信CMS 7 | 8 | 9 | 30 | 31 | 33 | 36 |
37 |
加载中...
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/FollowModal.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/components/SearchBar.vue: -------------------------------------------------------------------------------- 1 | 6 | 32 | 39 | -------------------------------------------------------------------------------- /src/js/cookie.js: -------------------------------------------------------------------------------- 1 | /** 2 | * cookie操作工具类,小程序端用Storage模拟cookie 3 | * cookie.set(name, value[, exSeconds]) 默认7天过期 4 | * cookie.get(name) 5 | * cookie.remove(name) 6 | */ 7 | 8 | /** 9 | * 设置cookie,默认7天过期 10 | * @param {*} cname 11 | * @param {*} cvalue 12 | * @param {*} exSeconds 过期时间(秒) 13 | */ 14 | function set(cname, cvalue, exSeconds = 7 * 24 * 60 * 60) { 15 | var d = new Date(); 16 | d.setTime(d.getTime() + (exSeconds * 1000)); 17 | var expires = "expires=" + d.toGMTString(); 18 | document.cookie = cname + "=" + cvalue + "; " + expires + "path=/"; 19 | } 20 | /** 21 | * 获取cookie 22 | * @param {*} cname 23 | */ 24 | function get(cname) { 25 | var name = cname + "="; 26 | var ca = document.cookie.split(';'); 27 | for (var i = 0; i < ca.length; i++) { 28 | var c = ca[i].trim(); 29 | if (c.indexOf(name) == 0) return c.substring(name.length, c.length); 30 | } 31 | return ""; 32 | } 33 | /** 34 | * 移除某项cookie 35 | * @param {*} cname 36 | */ 37 | function remove(cname) { 38 | set(cname, '', -365) 39 | } 40 | 41 | 42 | export default { 43 | get, set, remove 44 | } -------------------------------------------------------------------------------- /src/js/request.js: -------------------------------------------------------------------------------- 1 | // import fly from 'flyio' 2 | 3 | fly.config.timeout = 10000; 4 | 5 | //添加请求拦截器 6 | fly.interceptors.request.use((request) => { 7 | //可以显式返回request, 也可以不返回,没有返回值时拦截器中默认返回request 8 | return request; 9 | }) 10 | 11 | //添加响应拦截器,响应拦截器会在then/catch处理之前执行 12 | fly.interceptors.response.use( 13 | (response) => { 14 | if (response.status == 200) { 15 | let result = response.data; 16 | if (result.code != 0 && result.code != 200 && result.code != 400) { 17 | console.error(result.msg); 18 | } 19 | return result 20 | } else { 21 | console.error('服务端出错'); 22 | } 23 | }, 24 | (err) => { 25 | //发生网络错误后会走到这里 26 | console.error('网络错误!'); 27 | } 28 | ) 29 | 30 | export default fly 31 | -------------------------------------------------------------------------------- /src/js/wxAuth.js: -------------------------------------------------------------------------------- 1 | import fly from '../js/request'; 2 | import cookie from './cookie' 3 | const APPID = process.env.VUE_APP_WX_APPID; 4 | const WX_AUTH_URL = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=' + APPID 5 | + '&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_base&state=0#wechat_redirect'; 6 | 7 | /** 8 | * 微信授权 9 | */ 10 | export default function wxAuth() { 11 | return new Promise((resolve, reject) => { 12 | let openid = cookie.get('openid'); 13 | if (openid) { 14 | resolve(openid); 15 | return; 16 | } 17 | let code = getUrlParam('code'); 18 | if (!code) {//未经过微信授权 19 | let currentUrl = encodeURIComponent(window.location.href); 20 | window.location.replace(WX_AUTH_URL.replace('REDIRECT_URI', currentUrl)); 21 | } else { 22 | fly.post('/wx/wxAuth/codeToOpenid', { 23 | code: code 24 | }).then(res => { 25 | if (res.code == 200) { 26 | console.log("微信授权完成"); 27 | resolve(res.data); 28 | } else { 29 | console.log("换取openid失败"); 30 | } 31 | }); 32 | } 33 | }); 34 | } 35 | 36 | function getUrlParam(key) {//获取当前页面url中的参数 37 | var reg = new RegExp("(^|&)" + key + "=([^&]*)(&|$)"); 38 | var result = window.location.search.substr(1).match(reg); 39 | return result ? decodeURIComponent(result[2]) : ''; 40 | } 41 | -------------------------------------------------------------------------------- /src/js/wxShare.js: -------------------------------------------------------------------------------- 1 | import fly from '../js/request'; 2 | 3 | // 朋友圈 4 | let title = '微信CMS-By Niefy'; 5 | let imgUrl = 'https://avatar.gitee.com/uploads/43/1028743_niefy.png'; 6 | const defaultDesc = '这是一个极简的CMS系统!'; 7 | export default function wxShare(path = '/', desc = defaultDesc) { 8 | if (!isWeixinBrowser()) { 9 | return; 10 | } 11 | let shareUrl = window.location.origin + '/client/' + window.location.hash; 12 | loadShareSignature(); 13 | wx.ready(function () { 14 | console.log('shareUrl:' + shareUrl); 15 | // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。 16 | wx.onMenuShareTimeline({ 17 | title: '微信CMS', // 分享标题 18 | link: shareUrl, // 分享链接 19 | imgUrl: imgUrl, // 分享图标 20 | }); 21 | // 朋友 22 | wx.onMenuShareAppMessage({ 23 | title: title, // 分享标题 24 | desc: desc, // 分享描述 25 | link: shareUrl, // 分享链接 26 | imgUrl: imgUrl, // 分享图标 27 | }); 28 | }); 29 | wx.error(p => { 30 | console.log(p) 31 | }); 32 | } 33 | 34 | function loadShareSignature() { 35 | if (sessionStorage.shareSignature) { 36 | let shareSignature = JSON.parse(sessionStorage.shareSignature); 37 | setShareConfig(shareSignature); 38 | return; 39 | } 40 | fly.get('/wx/wxAuth/getShareSignature',null,{ 41 | headers:{ 42 | 'wx-client-href':location.href 43 | } 44 | }).then((res) => { 45 | if (res.code != 200) { 46 | return 47 | } 48 | sessionStorage.shareSignature = JSON.stringify(res.data); 49 | setShareConfig(res.data); 50 | }); 51 | } 52 | 53 | function setShareConfig(shareSignature) { 54 | wx.config({ 55 | debug: false, 56 | appId: shareSignature.appId, 57 | timestamp: shareSignature.wxTimestamp, 58 | nonceStr: shareSignature.wxNoncestr, 59 | signature: shareSignature.wxSignature, 60 | jsApiList: [ 61 | 'checkJsApi', 62 | 'onMenuShareTimeline', 63 | 'onMenuShareAppMessage'] 64 | }); 65 | } 66 | 67 | function isWeixinBrowser() { 68 | var ua = window.navigator.userAgent.toLowerCase(); 69 | if (ua.match(/MicroMessenger/i) == 'micromessenger') { 70 | return true; 71 | } else { 72 | console.info('非微信浏览器'); 73 | return false; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import request from './js/request' 5 | import wxShare from './js/wxShare' 6 | 7 | Vue.config.productionTip = false 8 | Vue.prototype.$fly = request 9 | Vue.prototype.$wxShare = wxShare 10 | 11 | new Vue({ 12 | router, 13 | render: h => h(App) 14 | }).$mount('#app') 15 | 16 | vant.Toast.setDefaultOptions({position:'bottom'}) -------------------------------------------------------------------------------- /src/pages/Article.vue: -------------------------------------------------------------------------------- 1 | 14 | 60 | 61 | -------------------------------------------------------------------------------- /src/pages/Home.vue: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /src/pages/NotFound.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/QuestionCategory.vue: -------------------------------------------------------------------------------- 1 | 10 | 71 | 77 | -------------------------------------------------------------------------------- /src/pages/QuestionSearch.vue: -------------------------------------------------------------------------------- 1 | 11 | 65 | 71 | -------------------------------------------------------------------------------- /src/pages/Subscribe.vue: -------------------------------------------------------------------------------- 1 | 19 | 110 | 129 | -------------------------------------------------------------------------------- /src/pages/WxLogin.vue: -------------------------------------------------------------------------------- 1 | 8 | 32 | 37 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | // import Vue from 'vue' 2 | // import Router from 'vue-router' 3 | 4 | // Vue.use(Router) 5 | 6 | const router = new VueRouter({ 7 | routes: [ 8 | { 9 | path: '/', 10 | component: () => import('@/pages/Home'), 11 | meta: { title: '发现' } 12 | }, { 13 | path: '/wxLogin', 14 | component: () => import('@/pages/WxLogin'), 15 | meta: { title: '微信登录示例' } 16 | },{ 17 | path: '/questionSearch', 18 | component: () => import('@/pages/QuestionSearch'), 19 | meta: { title: '帮助中心' } 20 | }, { 21 | path: '/questionCategory', 22 | component: () => import('@/pages/QuestionCategory'), 23 | meta: { title: '帮助中心' } 24 | }, { 25 | path: '/article/:articleId', 26 | component: () => import('@/pages/Article'), 27 | meta: { title: '文章详情' } 28 | }, { 29 | path: '/subscribe', 30 | component: () => import('@/pages/Subscribe'), 31 | meta: { title: '消息订阅' } 32 | }, { 33 | path: '/*', 34 | component: () => import('@/pages/NotFound'), 35 | meta: { title: '404' } 36 | } 37 | ] 38 | }) 39 | 40 | Vue.prototype.$go = function (link) { 41 | if (/^(http|https):.*$/.test(link)) {//打开外部链接 42 | window.location.href = link; 43 | } else { 44 | router.push(link); 45 | } 46 | } 47 | export default router; -------------------------------------------------------------------------------- /src/style/article.css: -------------------------------------------------------------------------------- 1 | /* html相关样式 */ 2 | #content a { 3 | color: #4285f4; 4 | } 5 | #content h1,#content h2,#content h3,#content h4,#content h5,#content h6{ 6 | margin: 0.3rem 0; 7 | color: #0064A8; 8 | line-height: 2rem; 9 | } 10 | #content h1{ 11 | font-size: 1.4rem; 12 | } 13 | #content h2{ 14 | font-size: 1.2rem; 15 | } 16 | #content h3{ 17 | font-size: 1.1rem; 18 | } 19 | #content h4, 20 | #content h5, 21 | #content h6 { 22 | font-size: 1rem; 23 | } 24 | 25 | #content hr { 26 | height: 0.2em; 27 | border: 0; 28 | color: #CCCCCC; 29 | background-color: #CCCCCC; 30 | } 31 | 32 | #content p, 33 | #content blockquote, 34 | #content ul, 35 | #content ol, 36 | #content dl, 37 | #content li, 38 | #content table, 39 | #content pre { 40 | margin: 8px 0; 41 | } 42 | 43 | #content p { 44 | margin: 1em 0; 45 | line-height: 1.5rem; 46 | } 47 | 48 | #content pre { 49 | background-color: #F8F8F8; 50 | border: 1px solid #CCCCCC; 51 | border-radius: 3px; 52 | overflow: auto; 53 | padding: 5px; 54 | } 55 | 56 | #content blockquote { 57 | color: #666666; 58 | margin: 0; 59 | border-left: 0.2em #EEE solid; 60 | } 61 | 62 | #content ul, 63 | #content ol { 64 | margin: 1em 0; 65 | padding: 0 0 0 2em; 66 | } 67 | 68 | #content li p:last-child { 69 | margin: 0 70 | } 71 | 72 | #content dd { 73 | margin: 0 0 0 2em; 74 | } 75 | 76 | #content img { 77 | border: 0; 78 | max-width: 100%; 79 | display: block; 80 | object-fit: contain; 81 | width: auto !important; 82 | height: auto !important; 83 | } 84 | 85 | #content table { 86 | border-collapse: collapse; 87 | border-spacing: 0; 88 | width: 100%; 89 | border: 1px solid #eee; 90 | } 91 | 92 | #content td { 93 | vertical-align: top; 94 | padding: 0.2em 0; 95 | border-top: 1px solid #EEEEEE; 96 | } -------------------------------------------------------------------------------- /src/style/common.css: -------------------------------------------------------------------------------- 1 | 2 | /* -- 内外边距 -- */ 3 | 4 | .margin-0 { 5 | margin: 0; 6 | } 7 | 8 | .margin-xs { 9 | margin: 5px; 10 | } 11 | 12 | .margin-sm { 13 | margin: 10px; 14 | } 15 | 16 | .margin { 17 | margin: 15px; 18 | } 19 | 20 | .margin-lg { 21 | margin: 20px; 22 | } 23 | 24 | .margin-xl { 25 | margin: 25px; 26 | } 27 | 28 | .margin-top-xs { 29 | margin-top: 5px; 30 | } 31 | 32 | .margin-top-sm { 33 | margin-top: 10px; 34 | } 35 | 36 | .margin-top { 37 | margin-top: 15px; 38 | } 39 | 40 | .margin-top-lg { 41 | margin-top: 20px; 42 | } 43 | 44 | .margin-top-xl { 45 | margin-top: 25px; 46 | } 47 | 48 | .margin-right-xs { 49 | margin-right: 5px; 50 | } 51 | 52 | .margin-right-sm { 53 | margin-right: 10px; 54 | } 55 | 56 | .margin-right { 57 | margin-right: 15px; 58 | } 59 | 60 | .margin-right-lg { 61 | margin-right: 20px; 62 | } 63 | 64 | .margin-right-xl { 65 | margin-right: 25px; 66 | } 67 | 68 | .margin-bottom-xs { 69 | margin-bottom: 5px; 70 | } 71 | 72 | .margin-bottom-sm { 73 | margin-bottom: 10px; 74 | } 75 | 76 | .margin-bottom { 77 | margin-bottom: 15px; 78 | } 79 | 80 | .margin-bottom-lg { 81 | margin-bottom: 20px; 82 | } 83 | 84 | .margin-bottom-xl { 85 | margin-bottom: 25px; 86 | } 87 | 88 | .margin-left-xs { 89 | margin-left: 5px; 90 | } 91 | 92 | .margin-left-sm { 93 | margin-left: 10px; 94 | } 95 | 96 | .margin-left { 97 | margin-left: 15px; 98 | } 99 | 100 | .margin-left-lg { 101 | margin-left: 20px; 102 | } 103 | 104 | .margin-left-xl { 105 | margin-left: 25px; 106 | } 107 | 108 | .margin-lr-xs { 109 | margin-left: 5px; 110 | margin-right: 5px; 111 | } 112 | 113 | .margin-lr-sm { 114 | margin-left: 10px; 115 | margin-right: 10px; 116 | } 117 | 118 | .margin-lr { 119 | margin-left: 15px; 120 | margin-right: 15px; 121 | } 122 | 123 | .margin-lr-lg { 124 | margin-left: 20px; 125 | margin-right: 20px; 126 | } 127 | 128 | .margin-lr-xl { 129 | margin-left: 25px; 130 | margin-right: 25px; 131 | } 132 | 133 | .margin-tb-xs { 134 | margin-top: 5px; 135 | margin-bottom: 5px; 136 | } 137 | 138 | .margin-tb-sm { 139 | margin-top: 10px; 140 | margin-bottom: 10px; 141 | } 142 | 143 | .margin-tb { 144 | margin-top: 15px; 145 | margin-bottom: 15px; 146 | } 147 | 148 | .margin-tb-lg { 149 | margin-top: 20px; 150 | margin-bottom: 20px; 151 | } 152 | 153 | .margin-tb-xl { 154 | margin-top: 25px; 155 | margin-bottom: 25px; 156 | } 157 | 158 | .padding-0 { 159 | padding: 0; 160 | } 161 | 162 | .padding-xs { 163 | padding: 5px; 164 | } 165 | 166 | .padding-sm { 167 | padding: 10px; 168 | } 169 | 170 | .padding { 171 | padding: 15px; 172 | } 173 | 174 | .padding-lg { 175 | padding: 20px; 176 | } 177 | 178 | .padding-xl { 179 | padding: 25px; 180 | } 181 | 182 | .padding-top-xs { 183 | padding-top: 5px; 184 | } 185 | 186 | .padding-top-sm { 187 | padding-top: 10px; 188 | } 189 | 190 | .padding-top { 191 | padding-top: 15px; 192 | } 193 | 194 | .padding-top-lg { 195 | padding-top: 20px; 196 | } 197 | 198 | .padding-top-xl { 199 | padding-top: 25px; 200 | } 201 | 202 | .padding-right-xs { 203 | padding-right: 5px; 204 | } 205 | 206 | .padding-right-sm { 207 | padding-right: 10px; 208 | } 209 | 210 | .padding-right { 211 | padding-right: 15px; 212 | } 213 | 214 | .padding-right-lg { 215 | padding-right: 20px; 216 | } 217 | 218 | .padding-right-xl { 219 | padding-right: 25px; 220 | } 221 | 222 | .padding-bottom-xs { 223 | padding-bottom: 5px; 224 | } 225 | 226 | .padding-bottom-sm { 227 | padding-bottom: 10px; 228 | } 229 | 230 | .padding-bottom { 231 | padding-bottom: 15px; 232 | } 233 | 234 | .padding-bottom-lg { 235 | padding-bottom: 20px; 236 | } 237 | 238 | .padding-bottom-xl { 239 | padding-bottom: 25px; 240 | } 241 | 242 | .padding-left-xs { 243 | padding-left: 5px; 244 | } 245 | 246 | .padding-left-sm { 247 | padding-left: 10px; 248 | } 249 | 250 | .padding-left { 251 | padding-left: 15px; 252 | } 253 | 254 | .padding-left-lg { 255 | padding-left: 20px; 256 | } 257 | 258 | .padding-left-xl { 259 | padding-left: 25px; 260 | } 261 | 262 | .padding-lr-xs { 263 | padding-left: 5px; 264 | padding-right: 5px; 265 | } 266 | 267 | .padding-lr-sm { 268 | padding-left: 10px; 269 | padding-right: 10px; 270 | } 271 | 272 | .padding-lr { 273 | padding-left: 15px; 274 | padding-right: 15px; 275 | } 276 | 277 | .padding-lr-lg { 278 | padding-left: 20px; 279 | padding-right: 20px; 280 | } 281 | 282 | .padding-lr-xl { 283 | padding-left: 25px; 284 | padding-right: 25px; 285 | } 286 | 287 | .padding-tb-xs { 288 | padding-top: 5px; 289 | padding-bottom: 5px; 290 | } 291 | 292 | .padding-tb-sm { 293 | padding-top: 10px; 294 | padding-bottom: 10px; 295 | } 296 | 297 | .padding-tb { 298 | padding-top: 15px; 299 | padding-bottom: 15px; 300 | } 301 | 302 | .padding-tb-lg { 303 | padding-top: 20px; 304 | padding-bottom: 20px; 305 | } 306 | 307 | .padding-tb-xl { 308 | padding-top: 25px; 309 | padding-bottom: 25px; 310 | } 311 | 312 | /* -- 浮动 -- */ 313 | 314 | .cf::after, 315 | .cf::before { 316 | content: " "; 317 | display: table; 318 | } 319 | 320 | .cf::after { 321 | clear: both; 322 | } 323 | 324 | .fl { 325 | float: left; 326 | } 327 | 328 | .fr { 329 | float: right; 330 | } 331 | 332 | /* ================== 333 | 背景 334 | ==================== */ 335 | 336 | .line-red::after, 337 | .lines-red::after { 338 | border-color: #e54d42; 339 | } 340 | 341 | .line-orange::after, 342 | .lines-orange::after { 343 | border-color: #f37b1d; 344 | } 345 | 346 | .line-yellow::after, 347 | .lines-yellow::after { 348 | border-color: #fbbd08; 349 | } 350 | 351 | .line-olive::after, 352 | .lines-olive::after { 353 | border-color: #8dc63f; 354 | } 355 | 356 | .line-green::after, 357 | .lines-green::after { 358 | border-color: #39b54a; 359 | } 360 | 361 | .line-cyan::after, 362 | .lines-cyan::after { 363 | border-color: #1cbbb4; 364 | } 365 | 366 | .line-blue::after, 367 | .lines-blue::after { 368 | border-color: #0081ff; 369 | } 370 | 371 | .line-purple::after, 372 | .lines-purple::after { 373 | border-color: #6739b6; 374 | } 375 | 376 | .line-mauve::after, 377 | .lines-mauve::after { 378 | border-color: #9c26b0; 379 | } 380 | 381 | .line-pink::after, 382 | .lines-pink::after { 383 | border-color: #e03997; 384 | } 385 | 386 | .line-brown::after, 387 | .lines-brown::after { 388 | border-color: #a5673f; 389 | } 390 | 391 | .line-grey::after, 392 | .lines-grey::after { 393 | border-color: #8799a3; 394 | } 395 | 396 | .line-gray::after, 397 | .lines-gray::after { 398 | border-color: #aaaaaa; 399 | } 400 | 401 | .line-black::after, 402 | .lines-black::after { 403 | border-color: #333333; 404 | } 405 | 406 | .line-white::after, 407 | .lines-white::after { 408 | border-color: #ffffff; 409 | } 410 | 411 | .bg-red { 412 | background-color: #e54d42; 413 | color: #ffffff; 414 | } 415 | 416 | .bg-orange { 417 | background-color: #f37b1d; 418 | color: #ffffff; 419 | } 420 | 421 | .bg-yellow { 422 | background-color: #fbbd08; 423 | color: #333333; 424 | } 425 | 426 | .bg-olive { 427 | background-color: #8dc63f; 428 | color: #ffffff; 429 | } 430 | 431 | .bg-green { 432 | background-color: #39b54a; 433 | color: #ffffff; 434 | } 435 | 436 | .bg-cyan { 437 | background-color: #1cbbb4; 438 | color: #ffffff; 439 | } 440 | 441 | .bg-blue { 442 | background-color: #0081ff; 443 | color: #ffffff; 444 | } 445 | 446 | .bg-purple { 447 | background-color: #6739b6; 448 | color: #ffffff; 449 | } 450 | 451 | .bg-mauve { 452 | background-color: #9c26b0; 453 | color: #ffffff; 454 | } 455 | 456 | .bg-pink { 457 | background-color: #e03997; 458 | color: #ffffff; 459 | } 460 | 461 | .bg-brown { 462 | background-color: #a5673f; 463 | color: #ffffff; 464 | } 465 | 466 | .bg-grey { 467 | background-color: #8799a3; 468 | color: #ffffff; 469 | } 470 | 471 | .bg-gray { 472 | background-color: #f0f0f0; 473 | color: #333333; 474 | } 475 | 476 | .bg-black { 477 | background-color: #333333; 478 | color: #ffffff; 479 | } 480 | 481 | .bg-white { 482 | background-color: #ffffff; 483 | color: #666666; 484 | } 485 | 486 | .bg-shadeTop { 487 | background-image: linear-gradient(rgba(0, 0, 0, 1), rgba(0, 0, 0, 0.01)); 488 | color: #ffffff; 489 | } 490 | 491 | .bg-shadeBottom { 492 | background-image: linear-gradient(rgba(0, 0, 0, 0.01), rgba(0, 0, 0, 1)); 493 | color: #ffffff; 494 | } 495 | 496 | .bg-red.light { 497 | color: #e54d42; 498 | background-color: #fadbd9; 499 | } 500 | 501 | .bg-orange.light { 502 | color: #f37b1d; 503 | background-color: #fde6d2; 504 | } 505 | 506 | .bg-yellow.light { 507 | color: #fbbd08; 508 | background-color: #fef2ced2; 509 | } 510 | 511 | .bg-olive.light { 512 | color: #8dc63f; 513 | background-color: #e8f4d9; 514 | } 515 | 516 | .bg-green.light { 517 | color: #39b54a; 518 | background-color: #d7f0dbff; 519 | } 520 | 521 | .bg-cyan.light { 522 | color: #1cbbb4; 523 | background-color: #d2f1f0; 524 | } 525 | 526 | .bg-blue.light { 527 | color: #0081ff; 528 | background-color: #cce6ff; 529 | } 530 | 531 | .bg-purple.light { 532 | color: #6739b6; 533 | background-color: #e1d7f0; 534 | } 535 | 536 | .bg-mauve.light { 537 | color: #9c26b0; 538 | background-color: #ebd4ef; 539 | } 540 | 541 | .bg-pink.light { 542 | color: #e03997; 543 | background-color: #f9d7ea; 544 | } 545 | 546 | .bg-brown.light { 547 | color: #a5673f; 548 | background-color: #ede1d9; 549 | } 550 | 551 | .bg-grey.light { 552 | color: #8799a3; 553 | background-color: #e7ebed; 554 | } 555 | 556 | .bg-gradual-red { 557 | background-image: linear-gradient(45deg, #f43f3b, #ec008c); 558 | color: #ffffff; 559 | } 560 | 561 | .bg-gradual-orange { 562 | background-image: linear-gradient(45deg, #ff9700, #ed1c24); 563 | color: #ffffff; 564 | } 565 | 566 | .bg-gradual-green { 567 | background-image: linear-gradient(45deg, #39b54a, #8dc63f); 568 | color: #ffffff; 569 | } 570 | 571 | .bg-gradual-purple { 572 | background-image: linear-gradient(45deg, #9000ff, #5e00ff); 573 | color: #ffffff; 574 | } 575 | 576 | .bg-gradual-pink { 577 | background-image: linear-gradient(45deg, #ec008c, #6739b6); 578 | color: #ffffff; 579 | } 580 | 581 | .bg-gradual-blue { 582 | background-image: linear-gradient(45deg, #0081ff, #1cbbb4); 583 | color: #ffffff; 584 | } 585 | 586 | 587 | /* ================== 588 | 文本 589 | ==================== */ 590 | 591 | .text-xs { 592 | font-size: 10px; 593 | } 594 | 595 | .text-sm { 596 | font-size: 12px; 597 | } 598 | 599 | .text-df { 600 | font-size: 14px; 601 | } 602 | 603 | .text-lg { 604 | font-size: 16px; 605 | } 606 | 607 | .text-xl { 608 | font-size: 18px; 609 | } 610 | 611 | .text-xxl { 612 | font-size: 22px; 613 | } 614 | 615 | .text-sl { 616 | font-size: 40px; 617 | } 618 | 619 | .text-xsl { 620 | font-size: 60px; 621 | } 622 | 623 | .text-cut { 624 | text-overflow: ellipsis; 625 | white-space: nowrap; 626 | overflow: hidden; 627 | } 628 | 629 | .text-bold { 630 | font-weight: bold; 631 | } 632 | 633 | .text-center { 634 | text-align: center; 635 | } 636 | 637 | .text-content { 638 | line-height: 1.6; 639 | } 640 | 641 | .text-left { 642 | text-align: left; 643 | } 644 | 645 | .text-right { 646 | text-align: right; 647 | } 648 | 649 | .text-red, 650 | .line-red, 651 | .lines-red { 652 | color: #e54d42; 653 | } 654 | 655 | .text-orange, 656 | .line-orange, 657 | .lines-orange { 658 | color: #f37b1d; 659 | } 660 | 661 | .text-yellow, 662 | .line-yellow, 663 | .lines-yellow { 664 | color: #fbbd08; 665 | } 666 | 667 | .text-olive, 668 | .line-olive, 669 | .lines-olive { 670 | color: #8dc63f; 671 | } 672 | 673 | .text-green, 674 | .line-green, 675 | .lines-green { 676 | color: #39b54a; 677 | } 678 | 679 | .text-cyan, 680 | .line-cyan, 681 | .lines-cyan { 682 | color: #1cbbb4; 683 | } 684 | 685 | .text-blue, 686 | .line-blue, 687 | .lines-blue { 688 | color: #0081ff; 689 | } 690 | 691 | .text-purple, 692 | .line-purple, 693 | .lines-purple { 694 | color: #6739b6; 695 | } 696 | 697 | .text-mauve, 698 | .line-mauve, 699 | .lines-mauve { 700 | color: #9c26b0; 701 | } 702 | 703 | .text-pink, 704 | .line-pink, 705 | .lines-pink { 706 | color: #e03997; 707 | } 708 | 709 | .text-brown, 710 | .line-brown, 711 | .lines-brown { 712 | color: #a5673f; 713 | } 714 | 715 | .text-grey, 716 | .line-grey, 717 | .lines-grey { 718 | color: #8799a3; 719 | } 720 | 721 | .text-gray, 722 | .line-gray, 723 | .lines-gray { 724 | color: #aaaaaa; 725 | } 726 | 727 | .text-black, 728 | .line-black, 729 | .lines-black { 730 | color: #333333; 731 | } 732 | 733 | .text-white, 734 | .line-white, 735 | .lines-white { 736 | color: #ffffff; 737 | } -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: './', 3 | assetsDir: undefined, 4 | productionSourceMap: false, 5 | 6 | devServer: { 7 | proxy: { 8 | '/wx': { 9 | target: 'http://127.0.0.1:8088' 10 | }, 11 | } 12 | }, 13 | 14 | outputDir: undefined, 15 | runtimeCompiler: undefined, 16 | parallel: undefined, 17 | configureWebpack:{ 18 | devServer: { 19 | historyApiFallback: true, 20 | allowedHosts:"all", 21 | }, 22 | externals: { 23 | vue: "Vue", 24 | "vue-router": "Router", 25 | "flyio": "Fly" 26 | } 27 | }, 28 | chainWebpack: config => { 29 | // 移除 prefetch 插件 30 | config.plugins.delete('prefetch') 31 | } 32 | } --------------------------------------------------------------------------------