├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.md │ ├── open-an-issue.md │ └── workflows │ │ └── master.yml └── workflows │ ├── dev.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README-zh_CN.md ├── README.md ├── fe ├── .babelrc.js ├── .editorconfig ├── .eslintrc.js ├── .prettierrc ├── Jenkinsfile ├── babel.config.js ├── build │ ├── ssl │ │ ├── ssl.crt │ │ └── ssl.key │ ├── webpack.base.config.js │ ├── webpack.dev.config.js │ └── webpack.prod.config.js ├── nginx.conf ├── package-lock.json ├── package.json ├── public │ └── index.html ├── publish.sh ├── src │ ├── App.vue │ ├── api │ │ ├── auth.ts │ │ ├── comment.ts │ │ ├── coupon.ts │ │ ├── index.ts │ │ ├── personal.ts │ │ ├── request.ts │ │ └── wechat.ts │ ├── components │ │ ├── column-divide │ │ │ └── index.vue │ │ ├── count-down │ │ │ └── index.vue │ │ ├── coupon-brief │ │ │ ├── images │ │ │ │ ├── quan_banklogo.png │ │ │ │ └── quan_logo.png │ │ │ └── index.vue │ │ ├── coupon-comment │ │ │ ├── images │ │ │ │ ├── notclickstar.png │ │ │ │ └── star.png │ │ │ └── index.vue │ │ ├── coupon-rule │ │ │ ├── images │ │ │ │ ├── flex_down.png │ │ │ │ └── flex_up.png │ │ │ └── index.vue │ │ ├── footer-nav │ │ │ ├── images │ │ │ │ ├── discountsblue.png │ │ │ │ ├── discountsgrey.png │ │ │ │ ├── mineblue.png │ │ │ │ └── minegrey.png │ │ │ └── index.vue │ │ ├── header-explain │ │ │ ├── goback.png │ │ │ └── index.vue │ │ ├── star │ │ │ └── index.vue │ │ └── upload │ │ │ ├── images │ │ │ ├── back.png │ │ │ ├── delete.png │ │ │ ├── grey.png │ │ │ └── upload-btn.png │ │ │ ├── index.ts │ │ │ ├── index.vue │ │ │ ├── styles │ │ │ └── upload.less │ │ │ └── upload.ts │ ├── libs │ │ ├── test │ │ │ └── index.js │ │ └── wx │ │ │ ├── ajax.ts │ │ │ ├── auth.ts │ │ │ └── index.ts │ ├── main.ts │ ├── mixins │ │ └── directive.ts │ ├── pages │ │ ├── account │ │ │ ├── get-phone-code.vue │ │ │ ├── images │ │ │ │ ├── account.png │ │ │ │ ├── flow1.png │ │ │ │ ├── flow2.png │ │ │ │ ├── password.png │ │ │ │ ├── phone.png │ │ │ │ └── verify.png │ │ │ ├── index.vue │ │ │ ├── login.vue │ │ │ ├── regist.vue │ │ │ ├── reset-password.vue │ │ │ └── tour-app-account.less │ │ ├── get-coupon │ │ │ ├── images │ │ │ │ ├── lingqu.png │ │ │ │ ├── pay_line.png │ │ │ │ ├── quan_cards.png │ │ │ │ ├── quan_er.png │ │ │ │ ├── quan_get.png │ │ │ │ ├── quan_line.png │ │ │ │ └── quan_plogo.png │ │ │ └── index.vue │ │ ├── home.vue │ │ ├── personal │ │ │ ├── change-headpic.vue │ │ │ ├── change-user-name.vue │ │ │ ├── change-user-sex.vue │ │ │ ├── images │ │ │ │ ├── check.png │ │ │ │ ├── flex_down.png │ │ │ │ ├── flex_up.png │ │ │ │ ├── head.png │ │ │ │ ├── install.png │ │ │ │ ├── jinnangtuan.png │ │ │ │ ├── list_dutyfree.png │ │ │ │ ├── list_unionpay.png │ │ │ │ ├── list_visa.png │ │ │ │ ├── minebg.png │ │ │ │ ├── personal_head.png │ │ │ │ ├── personal_headbg.png │ │ │ │ ├── phone.png │ │ │ │ ├── right.png │ │ │ │ ├── sex.png │ │ │ │ └── username.png │ │ │ ├── index.vue │ │ │ ├── personal-edit.vue │ │ │ └── tour-app-personal.less │ │ └── wechat │ │ │ ├── auth.vue │ │ │ └── index.vue │ ├── router │ │ ├── index.ts │ │ └── routes.ts │ ├── static │ │ ├── css │ │ │ ├── swiper.min.css │ │ │ └── tour-app-base.css │ │ ├── images │ │ │ ├── check.png │ │ │ ├── circlered.png │ │ │ ├── discountbg.png │ │ │ ├── downarrows.png │ │ │ ├── goback.png │ │ │ ├── jnt.png │ │ │ ├── littlebtn.png │ │ │ ├── pastduebg.png │ │ │ ├── pastduebtn.png │ │ │ ├── taizilogo.png │ │ │ ├── usebg.png │ │ │ └── usebtn.png │ │ ├── js │ │ │ ├── cookie.js │ │ │ ├── iframefileupload.js │ │ │ ├── swiper.min.js │ │ │ └── validatefileupload.js │ │ └── less │ │ │ └── coupon.less │ ├── store │ │ ├── index.ts │ │ └── modules │ │ │ └── app.ts │ ├── types │ │ ├── global.d.ts │ │ ├── shims-tsx.d.ts │ │ ├── shims-vue.d.ts │ │ ├── test.d.ts │ │ └── wechat.ts │ └── utils │ │ ├── index.ts │ │ └── validator.ts ├── tsconfig.json └── vue.config.js └── server ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── Jenkinsfile ├── README.md ├── migration ├── 1696406073873-update-user-default-avatar.ts ├── 1696413073247-update-tour-icon-path.ts ├── 1696418964257-update-coupon-endtime.ts └── 1696423773558-update-coupon_recived_num-in-coupon-table.ts ├── nginx.conf ├── package-lock.json ├── package.json ├── pm2.json ├── publish.sh ├── src ├── controller │ ├── auth.controller.ts │ ├── banner.controller.ts │ ├── classify.controller.ts │ ├── comment.controller.ts │ ├── coupon.controller.ts │ ├── index.ts │ ├── region.controller.ts │ └── user.controller.ts ├── data-source.ts ├── dto │ ├── banner.dto.ts │ ├── comment.dto.ts │ ├── coupon-user.dto.ts │ ├── coupon.dto.ts │ ├── index.ts │ └── user.dto.ts ├── entity │ ├── banner.entity.ts │ ├── classify.entity.ts │ ├── comment.entity.ts │ ├── coupon-user.entity.ts │ ├── coupon.entity.ts │ ├── feature.entity.ts │ ├── index.ts │ ├── region.entity.ts │ └── user.entity.ts ├── index.ts ├── middleware │ ├── cors.middleware.ts │ ├── error.middleware.ts │ ├── index.ts │ └── validate-request.middlelware.ts ├── repository │ ├── auth.repository.ts │ ├── banner.repository.ts │ ├── classify.repository.ts │ ├── comment.repository.ts │ ├── coupon-user.repository.ts │ ├── coupon.repository.ts │ ├── index.ts │ ├── region.repository.ts │ └── user.repository.ts └── router │ ├── auth.router.ts │ ├── banner.router.ts │ ├── classify.router.ts │ ├── comment.router.ts │ ├── coupon.router.ts │ ├── index.ts │ ├── region.router.ts │ └── user.router.ts ├── tour.sql └── tsconfig.json /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: VueNode official forum 4 | url: http://www.0351zhuangxiu.com 5 | about: For general questions, support requests and discussions 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for VueNode 4 | title: Suggest an idea for VueNode 5 | labels: enhancement 6 | assignees: zhaoyiming0803 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/open-an-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Open an issue 3 | about: For reporting bugs or errors in the VueNode 4 | title: 'Issue: Open an issue' 5 | labels: bug 6 | assignees: zhaoyiming0803 7 | 8 | --- 9 | 10 | 19 | 20 | - **Version**: 21 | 24 | 25 | - **Platform**: 26 | 29 | 30 | #### Severity: 31 | 39 | 40 | #### Description: 41 | 46 | 47 | #### Steps to reproduce the error: 48 | 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/workflows/master.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: VueNode CI 5 | 6 | on: 7 | push: 8 | branches: [ dev ] 9 | pull_request: 10 | branches: [ dev ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: 12 20 | registry-url: https://registry.npmjs.org/ 21 | - run: | 22 | echo 'ACCESS_KEY_ID = $ACCESS_KEY_ID, ACCESS_KEY_SECRET = $ACCESS_KEY_SECRET' 23 | env: 24 | ACCESS_KEY_ID: ${{ secrets.oss.ACCESS_KEY_ID }} 25 | ACCESS_KEY_SECRET: ${{ secrets.oss.ACCESS_KEY_SECRET }} 26 | -------------------------------------------------------------------------------- /.github/workflows/dev.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: VueNode CI 5 | 6 | on: 7 | push: 8 | branches: [ dev ] 9 | pull_request: 10 | branches: [ dev ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: 12 20 | registry-url: https://registry.npmjs.org/ 21 | - run: | 22 | echo "ACCESS_KEY_ID = ${ACCESS_KEY_ID}, ACCESS_KEY_SECRET = ${ACCESS_KEY_SECRET}" 23 | env: 24 | ACCESS_KEY_ID: 123456789 25 | ACCESS_KEY_SECRET: ${{ secrets.ACCESS_KEY_SECRET }} 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: VueNode Test CI 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: 12 16 | registry-url: https://registry.npmjs.org/ 17 | - run: | 18 | echo "Test" 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log* 2 | .cache 3 | .DS_Store 4 | .idea 5 | .vscode 6 | node_modules 7 | yarn.lock 8 | dist 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 赵一鸣 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README-zh_CN.md: -------------------------------------------------------------------------------- 1 | # VueNode 2 | 3 | > 最好的学习方法是阅读书籍和官方文档,以及 GitHub 开源代码,然后编写测试代码。 4 | 5 | 简体中文 | [English](./README.md) 6 | 7 | 当前项目使用 Vue3 & Node.js 开发,旧版本可参考: 8 | 9 | - [VueNode v1.x](https://github.com/zhaoyiming0803/VueNode/tree/v1.0) 10 | 11 | - [VueNode v2.x](https://github.com/zhaoyiming0803/VueNode/tree/v2.2.2) 12 | 13 | ### 用 Koa.js 替换 Express 测试 demo 14 | 15 | - https://github.com/zhaoyiming0803/test-code/blob/master/TypeORMDemoWithKoa/ 16 | 17 | ### Vue 从 v2 到 v3 升级指南: 18 | 19 | - [Vue 官方升级指南](https://v3-migration.vuejs.org/) 20 | 21 | - [vuex](https://next.vuex.vuejs.org/guide/) 22 | 23 | - [vue-router](https://next.router.vuejs.org/installation.html) 24 | 25 | - [升级 vantUI](https://vant-contrib.gitee.io/vant/v3/#/zh-CN/migrate-from-v2) 26 | 27 | - [vuex](https://next.vuex.vuejs.org/guide/) 28 | 29 | - [vue-router](https://next.router.vuejs.org/installation.html) 30 | 31 | - [测试 demo](https://github.com/zhaoyiming0803/vue3-webpack-demo) 32 | 33 | - 先用 Vue 官方脚手架初始化一个测试 demo,看下最新的包依赖版本是多少,然后当前项目中也安装相应版本的依赖。 34 | 35 | - 将 `fe/src/router/routes.ts` 中的路由都暂时注释掉,新增一个测试页面,然后采用渐进式的方式逐步迁移原有业务代码。 36 | 37 | - 将 `fe/src/components` 下的组件都转为 Vue3 的语法。 38 | 39 | - 逐步迁移 `fe/src/pages`。 40 | 41 | - 注:此项目纯属个人爱好及代码测试。 42 | 43 | ### 技术栈 44 | 45 | - 前端:@vue/cli@4.5.13、vue.js@3.2.16、vuex@4.0.2、vue-router@4.0.11、Less、ES6(7|8)、Webpack4、axios@0.19.0 46 | 47 | - 后端:Node.js(Koa.js)、MySQL、TypeORM、class-validator 48 | 49 | ### 项目本地运行方法 50 | 51 | - git clone https://github.com/zhaoyiming0803/VueNode.git 52 | 53 | - 前端代码在 fe 目录下,node 代码在 server 目录下,打开对应的目录,查看 package.json,npm 执行 对应的 script 即可。 54 | 55 | ### 线上部署 56 | 57 | - 前端:参考 fe 目录下的 nginx.conf、Jenkinsfile、publish.sh 58 | 59 | - 后端:参考 server 目录下的 nginx.conf、pm2.json、Jenkinsfile、publish.sh 60 | 61 | ### 关于数据库 62 | 63 | - 安装 MySQL 数据库,新建数据库tour,然后导入全部数据(/server/tour.sql) 64 | 65 | - 数据库 tour_user 表中的用户默认密码均为 123456 66 | 67 | ### 目标功能 68 | 69 | - [x] 登录、注册、密码修改(100%) 70 | - [x] 个人中心信息展示、资料修改(100%) 71 | - [x] 头像上传(100%) 72 | - [x] app首页(100%) 73 | - [x] app列表页——全球优惠券(100%) 74 | - [x] 展示国家与地区列表(100%) 75 | - [x] 每个国家与地区对应的优惠券、新闻、banner轮播图(100%) 76 | - [x] 领取优惠券(100%) 77 | - [x] 优惠券详情(100%) 78 | - [x] 使用优惠券(100%) 79 | - [x] 发布优惠券文字(100%) 80 | - [x] 星级评价组件(100%) 81 | - [x] 微信分享(100%)使用 nodejs 开发微信源码:https://github.com/zhaoyiming0803/wechat-nodejs 82 | 83 | ### 项目GIF图 84 | 85 | ![gif](https://github.com/zhaoyiming0803/VueNode/blob/v1.0/project.gif?raw=true) 86 | 87 | ### 说明 88 | 89 | - 如果对您有帮助,您可以点右上角 "Star" 支持一下 谢谢! ^_^ 90 | 91 | - 或者您可以 "follow" 一下,我会不断开源更多的有趣的项目 92 | 93 | - 如有问题请直接在 Issues 中提,或者您发现问题并有非常好的解决方案,欢迎 PR 👍 94 | 95 | ### 个人微信&QQ:1047832475 96 | 97 | 98 | 99 | ### 参考资料 100 | 101 | - https://vuejs.org/ 102 | - https://router.vuejs.org/installation.html 103 | - https://vuex.vuejs.org/guide/ 104 | - https://koajs.com/ 105 | - https://github.com/koajs/router/blob/master/API.md 106 | - https://www.npmjs.com/package/koa-swagger-decorator 107 | - https://typeorm.io/ 108 | - https://www.npmjs.com/package/class-validator 109 | - https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html 110 | - https://pm2.keymetrics.io/docs/usage/quick-start/ 111 | - https://github.com/zhaoyiming0803/test-code/blob/master/TypeORMDemoWithKoa/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VueNode 2 | 3 | > The best way to learn is to read books and official documents, as well as open source codes in GitHub, and then write test demos. 4 | 5 | English | [简体中文](./README-zh_CN.md) 6 | 7 | VueNode is developed using Vue3 & Node.js, and the old version can refer to: 8 | 9 | - [VueNode v1.x](https://github.com/zhaoyiming0803/VueNode/tree/v1.0) 10 | 11 | - [VueNode v2.x](https://github.com/zhaoyiming0803/VueNode/tree/v2.2.2) 12 | 13 | - [VueNode v3.x](https://github.com/zhaoyiming0803/VueNode/tree/v3.0.1) 14 | 15 | ### Replace Express with Koa.js 16 | 17 | - https://github.com/zhaoyiming0803/test-code/blob/master/TypeORMDemoWithKoa/ 18 | 19 | ### Vue upgrade guide from v2 to v3: 20 | 21 | - [Vue Official Upgrade Guide](https://v3-migration.vuejs.org/) 22 | 23 | - [vuex](https://next.vuex.vuejs.org/guide/) 24 | 25 | - [vue-router](https://next.router.vuejs.org/installation.html) 26 | 27 | - [upgrade vantUI](https://vant-contrib.gitee.io/vant/v3/#/zh-CN/migrate-from-v2) 28 | 29 | - [vuex](https://next.vuex.vuejs.org/guide/) 30 | 31 | - [vue-router](https://next.router.vuejs.org/installation.html) 32 | 33 | - [test demo](https://github.com/zhaoyiming0803/vue3-webpack-demo) 34 | 35 | - Initialize a demo using Vue official cli, confirm the latest package dependency version is, and then install the corresponding version of the dependencies in the current project. 36 | 37 | - Temporarily comment out the routes in `fe/src/router/routes.ts`, add a new test page, and gradually migrate the original business code. 38 | 39 | - Convert all components under `fe/src/components` to Vue 3 syntax. 40 | 41 | - Gradually migrate `fe/src/pages`. 42 | 43 | - Note: This project is purely a personal hobby and code testing. 44 | 45 | ### Technology Stacks 46 | 47 | - FE:@vue/cli@4.5.13、vue.js@3.2.16、vuex@4.0.2、vue-router@4.0.11、Less、ES6(7|8)、Webpack4、axios@0.19.0 48 | 49 | - BE:Node.js(Koa.js)、MySQL、TypeORM、class-validator 50 | 51 | ### Run project in local 52 | 53 | - git clone https://github.com/zhaoyiming0803/VueNode.git 54 | 55 | - Open directory `fe` and `server`, view package.json, use npm can execute the corresponding script. 56 | 57 | ### Production environment deployment 58 | 59 | - FE:refer to nginx.conf、Jenkinsfile、publish.sh under directory `fe`. 60 | 61 | - BE:refer to nginx.conf、pm2.json、Jenkinsfile、publish.sh under directory `server`. 62 | 63 | ### About SQL 64 | 65 | - Install MySQL database, create a new database named `tour`, and then import all data (/server/tour.sql). 66 | 67 | - The default password for users in the user table is 123456. 68 | 69 | ### Features 70 | 71 | - [x] Login、Regist、Modify password(100%) 72 | - [x] Personal page(100%) 73 | - [x] Update profile(100%) 74 | - [x] Home(100%) 75 | - [x] Home - list, global coupon(100%) 76 | - [x] Show countries and regions list(100%) 77 | - [x] Banners, coupons belong to country or region(100%) 78 | - [x] Claim coupons(100%) 79 | - [x] Coupon detail(100%) 80 | - [x] Use coupon(100%) 81 | - [x] Publish coupon detail(100%) 82 | - [x] Star comment component(100%) 83 | - [x] Wechat sharing(100%)using nodejs:https://github.com/zhaoyiming0803/wechat-nodejs 84 | 85 | ### Project GIF 86 | 87 | ![gif](https://github.com/zhaoyiming0803/VueNode/blob/v1.0/project.gif?raw=true) 88 | 89 | ### Thanks 90 | 91 | - If it is helpful to you, you can click on the `Star` or `Watch` in the upper right corner to support it. Thank you ^_^ 92 | 93 | - Alternatively, you can `follow` me and I will continue to open up more interesting projects. 94 | 95 | - If you have any problems, please directly raise them in issues, or if you find problems and have excellent solutions, welcome PR 👍 96 | 97 | ### My Wechat & QQ:1047832475 98 | 99 | 100 | 101 | ### Reference material 102 | 103 | - https://vuejs.org/ 104 | - https://router.vuejs.org/installation.html 105 | - https://vuex.vuejs.org/guide/ 106 | - https://koajs.com/ 107 | - https://github.com/koajs/router/blob/master/API.md 108 | - https://www.npmjs.com/package/koa-swagger-decorator 109 | - https://typeorm.io/ 110 | - https://www.npmjs.com/package/class-validator 111 | - https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html 112 | - https://pm2.keymetrics.io/docs/usage/quick-start/ 113 | -------------------------------------------------------------------------------- /fe/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | esmodules: true 8 | } 9 | } 10 | ] 11 | ], 12 | plugins: [ 13 | [ 14 | '@babel/plugin-transform-runtime' 15 | ], 16 | [ 17 | 'add-module-exports' 18 | ] 19 | ] 20 | } -------------------------------------------------------------------------------- /fe/.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 = true 10 | 11 | [Makefile] 12 | indent_style = tab -------------------------------------------------------------------------------- /fe/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | ecmaVersion: 'es2015' 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | rules: { 15 | indent: [ 16 | 'error', 17 | 2 18 | ], 19 | 'linebreak-style': [ 20 | 'error', 21 | 'unix' 22 | ], 23 | quotes: [ 24 | 'error', 25 | 'single' 26 | ], 27 | semi: [ 28 | 'error', 29 | 'never' 30 | ], 31 | "no-unused-vars": 'off', 32 | "@typescript-eslint/no-unused-vars": 'off', 33 | '@typescript-eslint/no-explicit-any': 'off', 34 | '@typescript-eslint/no-var-requires': 'off', 35 | '@typescript-eslint/no-empty-interface': 'off', 36 | '@typescript-eslint/no-namespace': 'off' 37 | } 38 | } -------------------------------------------------------------------------------- /fe/.prettierrc: -------------------------------------------------------------------------------- 1 | semi: false 2 | singleQuote: true 3 | printWidth: 80 4 | trailingComma: 'none' 5 | arrowParens: 'avoid' 6 | tabWidth: 2 7 | -------------------------------------------------------------------------------- /fe/Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | node { 4 | label "" 5 | customWorkspace 'workspace/tour-fe/' 6 | } 7 | } 8 | 9 | stages { 10 | stage('build') { 11 | steps { 12 | sh 'npm --registry=https://registry.npm.taobao.org install' 13 | sh 'npm --registry=https://registry.npm.taobao.org run build' 14 | } 15 | } 16 | stage('deploy') { 17 | steps { 18 | sh 'bash ./publish.sh' 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /fe/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /fe/build/ssl/ssl.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFqjCCBJKgAwIBAgIQAvYXa/KrquG2fRRLbIhDNDANBgkqhkiG9w0BAQsFADBy 3 | MQswCQYDVQQGEwJDTjElMCMGA1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywg 4 | SW5jLjEdMBsGA1UECxMURG9tYWluIFZhbGlkYXRlZCBTU0wxHTAbBgNVBAMTFFRy 5 | dXN0QXNpYSBUTFMgUlNBIENBMB4XDTE5MDkxNDAwMDAwMFoXDTIwMDkxMzEyMDAw 6 | MFowIDEeMBwGA1UEAxMVd2ViLjAzNTF6aHVhbmd4aXUuY29tMIIBIjANBgkqhkiG 7 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Iuy1SR9Us2VoMx4qCG+jTVsW/zm9AzpJL9Z 8 | 0Me0WBa2j36ZApzUWuTzWJNE/1GZHeWKBodsRjG/pdP32LXIg81YfFgEk/5Pgex/ 9 | 1vYd6TumYU2hNeyQrqvyPuHkiJniLX26U8be8QLBkN/w8Xz7+J0bbPFIuRA5Yigg 10 | U/7eQtnF7zAweEJIQHbR8aX7JQO82eOIbOMBEFCBtfgn24uDPLkJkE/444l9zvKP 11 | PtBF2sgbij6l/G/QONhr65XXC8Akiuvf7rUu3+V9Ju8YJAGtLADv2VFyfKntpcl7 12 | RHLAo/rsqn6r6AG78BOsynuh+9rLaZxUlKx80JkJrqEmEKZbTwIDAQABo4ICjDCC 13 | AogwHwYDVR0jBBgwFoAUf9OZ86BHDjEAVlYijrfMnt3KAYowHQYDVR0OBBYEFPef 14 | Vpx0KorFZnZwyNtYpuDfSJARMCAGA1UdEQQZMBeCFXdlYi4wMzUxemh1YW5neGl1 15 | LmNvbTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF 16 | BwMCMEwGA1UdIARFMEMwNwYJYIZIAYb9bAECMCowKAYIKwYBBQUHAgEWHGh0dHBz 17 | Oi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQIBMIGSBggrBgEFBQcBAQSB 18 | hTCBgjA0BggrBgEFBQcwAYYoaHR0cDovL3N0YXR1c2UuZGlnaXRhbGNlcnR2YWxp 19 | ZGF0aW9uLmNvbTBKBggrBgEFBQcwAoY+aHR0cDovL2NhY2VydHMuZGlnaXRhbGNl 20 | cnR2YWxpZGF0aW9uLmNvbS9UcnVzdEFzaWFUTFNSU0FDQS5jcnQwCQYDVR0TBAIw 21 | ADCCAQUGCisGAQQB1nkCBAIEgfYEgfMA8QB3AKS5CZC0GFgUh7sTosxncAo8NZgE 22 | +RvfuON3zQ7IDdwQAAABbS8qoIcAAAQDAEgwRgIhAMarlbwAAN8cqfc8rCoHnYPr 23 | p591EHMV5xNgQH05dNl6AiEAoRXkhN0N5Mndlu9BN00y3fj3yR2Yncy7Clj99jfB 24 | KdgAdgCHdb/nWXz4jEOZX73zbv9WjUdWNv9KtWDBtOr/XqCDDwAAAW0vKqErAAAE 25 | AwBHMEUCICWBww8hJmFsYWQz2RXskj6VorjTVDCVw603yyW6HnjAAiEAnxy8/y0W 26 | p1USz9Ey7Lj6AXJsXuojjM1ptH60XFngFv8wDQYJKoZIhvcNAQELBQADggEBAI8d 27 | /fNvuEJeG822lVJeKB+fY7Mu+qYXwSKNmdROqbKi65O11P1vhOgzQfWepwdu20Zv 28 | DpOcQVeYcye+LI++S525xvcnHEqiPrNIQtAnPM6qFKYq+f0e7G2MUi2W/hlR3TMH 29 | 1JMWrOrKssXGVAjN54gICwfrQ+ltLtQfrU6APptb2wL6ZVs+Q19mBSN+a6U5H2vh 30 | mUFmDWDf0zb00OJU0UHuNjsTN/WFw9mOx5rh1ntvbxM/OxSXWJiP+FmvyHdq3gUk 31 | tdOFoqbEo+852NqbcaLbB4FzrfjdcLFNF3z/MzDwIy8bGaQDqVdLLCaywwKz+6gj 32 | C7i8XEShaaPRp4w4sgI= 33 | -----END CERTIFICATE----- 34 | -----BEGIN CERTIFICATE----- 35 | MIIErjCCA5agAwIBAgIQBYAmfwbylVM0jhwYWl7uLjANBgkqhkiG9w0BAQsFADBh 36 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 37 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD 38 | QTAeFw0xNzEyMDgxMjI4MjZaFw0yNzEyMDgxMjI4MjZaMHIxCzAJBgNVBAYTAkNO 39 | MSUwIwYDVQQKExxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMR0wGwYDVQQL 40 | ExREb21haW4gVmFsaWRhdGVkIFNTTDEdMBsGA1UEAxMUVHJ1c3RBc2lhIFRMUyBS 41 | U0EgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgWa9X+ph+wAm8 42 | Yh1Fk1MjKbQ5QwBOOKVaZR/OfCh+F6f93u7vZHGcUU/lvVGgUQnbzJhR1UV2epJa 43 | e+m7cxnXIKdD0/VS9btAgwJszGFvwoqXeaCqFoP71wPmXjjUwLT70+qvX4hdyYfO 44 | JcjeTz5QKtg8zQwxaK9x4JT9CoOmoVdVhEBAiD3DwR5fFgOHDwwGxdJWVBvktnoA 45 | zjdTLXDdbSVC5jZ0u8oq9BiTDv7jAlsB5F8aZgvSZDOQeFrwaOTbKWSEInEhnchK 46 | ZTD1dz6aBlk1xGEI5PZWAnVAba/ofH33ktymaTDsE6xRDnW97pDkimCRak6CEbfe 47 | 3dXw6OV5AgMBAAGjggFPMIIBSzAdBgNVHQ4EFgQUf9OZ86BHDjEAVlYijrfMnt3K 48 | AYowHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDgYDVR0PAQH/BAQD 49 | AgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAG 50 | AQH/AgEAMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3Au 51 | ZGlnaWNlcnQuY29tMEIGA1UdHwQ7MDkwN6A1oDOGMWh0dHA6Ly9jcmwzLmRpZ2lj 52 | ZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RDQS5jcmwwTAYDVR0gBEUwQzA3Bglg 53 | hkgBhv1sAQIwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29t 54 | L0NQUzAIBgZngQwBAgEwDQYJKoZIhvcNAQELBQADggEBAK3dVOj5dlv4MzK2i233 55 | lDYvyJ3slFY2X2HKTYGte8nbK6i5/fsDImMYihAkp6VaNY/en8WZ5qcrQPVLuJrJ 56 | DSXT04NnMeZOQDUoj/NHAmdfCBB/h1bZ5OGK6Sf1h5Yx/5wR4f3TUoPgGlnU7EuP 57 | ISLNdMRiDrXntcImDAiRvkh5GJuH4YCVE6XEntqaNIgGkRwxKSgnU3Id3iuFbW9F 58 | UQ9Qqtb1GX91AJ7i4153TikGgYCdwYkBURD8gSVe8OAco6IfZOYt/TEwii1Ivi1C 59 | qnuUlWpsF1LdQNIdfbW3TSe0BhQa7ifbVIfvPWHYOu3rkg1ZeMo6XRU9B4n5VyJY 60 | RmE= 61 | -----END CERTIFICATE----- 62 | -------------------------------------------------------------------------------- /fe/build/ssl/ssl.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA0Iuy1SR9Us2VoMx4qCG+jTVsW/zm9AzpJL9Z0Me0WBa2j36Z 3 | ApzUWuTzWJNE/1GZHeWKBodsRjG/pdP32LXIg81YfFgEk/5Pgex/1vYd6TumYU2h 4 | NeyQrqvyPuHkiJniLX26U8be8QLBkN/w8Xz7+J0bbPFIuRA5YiggU/7eQtnF7zAw 5 | eEJIQHbR8aX7JQO82eOIbOMBEFCBtfgn24uDPLkJkE/444l9zvKPPtBF2sgbij6l 6 | /G/QONhr65XXC8Akiuvf7rUu3+V9Ju8YJAGtLADv2VFyfKntpcl7RHLAo/rsqn6r 7 | 6AG78BOsynuh+9rLaZxUlKx80JkJrqEmEKZbTwIDAQABAoIBABKFwzmiIY1tXBOp 8 | mt0tY2wi/r+sK+PTkmY5+/i1XBkSKgnUWokRs9y/hrZAck7js5/6Ugg4FqT6EzBf 9 | 2Mdt3I7e+gklP4GDg2f3QPgvPkMyZ0SE2Bje14+OucKIXrtxQNZTMJhIoBGI8fdI 10 | +fCrmKz2tfJhpdi7Y6q1BKthMOLVUA5FIyXvbK7yerhWCBARbnfdVujS2ms+ZS9x 11 | 5rqQfHlHuFA58XE8uunJddGHVy+YF29VbSOOYZMXhDqwfXtrOQJ4ab3SSAS+KVFR 12 | h5+/EjIaHrh5jzP/Mac8ai2KfpJ+TeoVLvufohezzOKcnxvAdYR8cfdzI5zG6d6m 13 | UBTiqtECgYEA8jxnoxOReOHSKYun+bNsZ8ZDAx1dABgN55nhbIB0YwDnA7NR1WME 14 | EkR7rJd8/Nt4YPx5Jtq0ajACbYcf1w9FKZb6Azocj23RYf5EC9EJj3m33uc4Y/FU 15 | 5nv1yfnjYgJDh6UB2gMb0sFwsEOeA1iD8MvJrvdKZODOEdiexol0QNECgYEA3GU7 16 | Hcp+ZX5cOQjcZQ3O5j+yMiqr3gBeM9lDyByMeFfXfooOpy9MGaoCy66RVNhvhINW 17 | 9EXtONxnokZRU+lNrx97IuE8Hjd/JLBQ/o+WRT3QBsg7Ns4xi1Gj9MIpzOAV3Dvs 18 | UiQON2DhksnH6ciLGP9E97NeVnrZV9WIOSs64h8CgYEAzbTqNZxafxMWC93jKbNq 19 | rb26Dp0S6w+CT1loC2ISdDjB9WyEY/eP74tkky6aH4io84OzxoEXkM1wYl7LdTAs 20 | haMGcVMaCdsyYkswsfA0dDjjIlGsm4LHnGtMUNb6d7KAcmJ37hGRwSowbh8dwq2a 21 | bhRBE2pBLOWTWahhPSxhIuECgYAs2DBCLIyxbBepx0LJERkzQmyoxoP4BQ0l8aRY 22 | GG8AoacIaWD35ajPZAdzmE6b+/oc9XiA9aWCN16i5znvH/6djoNIopnP8CzfszyX 23 | v3GtHxmv95gM28G6/l6lE8jblhD8ofjA8fMuk3jynDogOJ0M9gv7drTQVejZdWpl 24 | b4VoswKBgFU4IU9KFs0VEmyUtTmtcaaHA6BZ4kOk3psRPFlk1mfPVoB0Ij8VjNba 25 | 1OKpqDDuTHY9cGPjfSxhhD24nRuOecDIRTTMUe1KZQgHBtcM3t3v4SlhlzqhbKkK 26 | vxnC7W2cB2n7G8OlzyCpLclesZoffRjHmQsGmrmXFu6qd2kfZ7kf 27 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /fe/build/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * webpack 公共配置 3 | * @author zhaoyiming 4 | * @since 2019/09/14 5 | */ 6 | const { merge } = require('webpack-merge') 7 | const tsImportPluginFactory = require('ts-import-plugin') 8 | 9 | const path = require('path') 10 | const resolve = dir => path.resolve(__dirname, '../', dir) 11 | 12 | module.exports = config => { 13 | config.resolve.alias 14 | .set('public', resolve('public')) 15 | .set('@', resolve('src')) 16 | config.resolve.extensions 17 | .add('.less') 18 | .add('.css') 19 | 20 | config.plugins 21 | .delete('prefetch') 22 | .delete('preload') 23 | 24 | config.plugin('inline-source') 25 | .use(require('html-webpack-inline-source-plugin')) 26 | .after('html') 27 | 28 | config.plugin('script-ext') 29 | .use(require('script-ext-html-webpack-plugin'), [{ 30 | defaultAttribute: 'defer' 31 | }]) 32 | .after('html') 33 | 34 | config.plugin('html') 35 | .tap(args => { 36 | args[0].inlineSource = '.(app|chunk-vendors).*.(css)' 37 | args[0].minify = undefined 38 | args[0].var = { 39 | NODE_ENV: process.env.NODE_ENV 40 | } 41 | return args 42 | }) 43 | 44 | config.module.rule('ts').use('ts-loader').tap(options => { 45 | options = merge(options, { 46 | transpileOnly: true, 47 | getCustomTransformers: () => ({ 48 | before: [tsImportPluginFactory({ 49 | libraryName: 'vant', 50 | libraryDirectory: 'es', 51 | style: true 52 | })] 53 | }), 54 | compilerOptions: { 55 | module: 'es2015' 56 | } 57 | }) 58 | return options 59 | }) 60 | 61 | config.plugin('copy').tap(args => { 62 | args[0][0].from = resolve('src/static') 63 | args[0][0].to = 'static' 64 | return args 65 | }) 66 | } -------------------------------------------------------------------------------- /fe/build/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | module.exports = config => { 2 | config.devServer = { 3 | host: 'localhost', 4 | // host: 'web.0351zhuangxiu.com', 5 | // port: 443, 6 | // https: { 7 | // key: fs.readFileSync(path.resolve(__dirname, 'ssl/ssl.key')), 8 | // cert: fs.readFileSync(path.resolve(__dirname, 'ssl/ssl.crt')) 9 | // }, 10 | proxy: { 11 | '/tour/static': { 12 | sw: false, 13 | target: 'https://web.0351zhuangxiu.com:443', 14 | pathRewrite: { '^/tour/static': '/static' }, 15 | changeOrigin: false 16 | } 17 | }, 18 | disableHostCheck: true 19 | } 20 | } -------------------------------------------------------------------------------- /fe/build/webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | const TerserPlugin = require("terser-webpack-plugin") 2 | const CompressionPlugin = require('compression-webpack-plugin') 3 | 4 | module.exports = config => { 5 | config.devtool = false 6 | 7 | config.externals = { 8 | 'vue': 'Vue', 9 | 'vue-router': 'VueRouter', 10 | 'axios': 'axios', 11 | 'vuex': 'Vuex' 12 | } 13 | 14 | config.optimization.minimizer.push(new TerserPlugin()) 15 | 16 | config.plugins.push(new CompressionPlugin({ 17 | test: /\.(js|css|html|svg)$/, 18 | threshold: 10, 19 | deleteOriginalAssets: false 20 | })) 21 | } -------------------------------------------------------------------------------- /fe/nginx.conf: -------------------------------------------------------------------------------- 1 | # 仅是部分对于项目的配置,其他配置项可自定义 2 | 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | http { 8 | include /etc/nginx/conf.d/*.conf; 9 | 10 | server { 11 | listen 443 ssl http2 default_server; 12 | server_name web.0351zhuangxiu.com; 13 | 14 | gzip on; 15 | gzip_disable "msie6"; 16 | gzip_min_length 1k; 17 | gzip_vary on; 18 | gzip_types text/plain text/css application/json application/x-javascript application/javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon; 19 | 20 | ssl_certificate /etc/nginx/ssl/web.0351zhuangxiu.com/ssl.crt; 21 | ssl_certificate_key /etc/nginx/ssl/web.0351zhuangxiu.com/ssl.key; 22 | 23 | location ^~ /tour/ { 24 | root /work/web; 25 | try_files $uri $uri/ /tour/index.html; 26 | } 27 | 28 | location ~ .*\.(js|css|png|jpg|jpeg)$ { 29 | root /work/web; 30 | add_header Cache-Control max-age=600; 31 | } 32 | } 33 | 34 | server { 35 | listen 80; 36 | server_name web.0351zhuangxiu.com; 37 | rewrite ^(.*) https://$host$1 permanent; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /fe/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuenode", 3 | "version": "3.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "eslint --ext .ts src/*/**.ts", 9 | "lint:fix": "eslint --fix --ext .ts src/*/**.ts" 10 | }, 11 | "author": "zhaoyiming0803@gmail.com", 12 | "dependencies": { 13 | "core-js": "^3.6.5" 14 | }, 15 | "devDependencies": { 16 | "@babel/plugin-transform-modules-umd": "^7.2.0", 17 | "@typescript-eslint/eslint-plugin": "^5.16.0", 18 | "@typescript-eslint/parser": "^5.16.0", 19 | "@vue/babel-preset-app": "^4.5.13", 20 | "@vue/cli-plugin-babel": "~4.5.0", 21 | "@vue/cli-plugin-typescript": "^4.5.13", 22 | "@vue/cli-service": "~4.5.0", 23 | "@vue/compiler-sfc": "^3.0.0", 24 | "axios": "^0.21.2", 25 | "babel-eslint": "^10.1.0", 26 | "babel-plugin-add-module-exports": "^1.0.4", 27 | "compression-webpack-plugin": "^3.0.0", 28 | "copy-webpack-plugin": "^5.0.4", 29 | "eslint": "^8.7.0", 30 | "eslint-config-prettier": "^8.5.0", 31 | "eslint-plugin-prettier": "^4.0.0", 32 | "eslint-plugin-vue": "^7.0.0", 33 | "express": "^4.17.1", 34 | "html-webpack-inline-source-plugin": "^0.0.10", 35 | "less": "^3.9.0", 36 | "less-loader": "^5.0.0", 37 | "prettier": "^2.5.1", 38 | "script-ext-html-webpack-plugin": "^2.1.5", 39 | "ts-import-plugin": "^1.6.7", 40 | "typescript": "^4.4.3", 41 | "uglifyjs-webpack-plugin": "^2.2.0", 42 | "vant": "^3.2.3", 43 | "vue": "^3.2.16", 44 | "vue-router": "^4.0.11", 45 | "vuex": "^4.0.2", 46 | "webpack-merge": "^5.8.0" 47 | }, 48 | "postcss": { 49 | "plugins": { 50 | "autoprefixer": {} 51 | } 52 | }, 53 | "browserslist": [ 54 | "Android >= 4.0", 55 | "iOS >= 7" 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /fe/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 加载中... 9 | <% if (htmlWebpackPlugin.options.var.NODE_ENV === 'production'){ %> 10 | 11 | <% } %> 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /fe/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # publish 4 | cd ./dist 5 | tar -czvf tour.tar.gz . 6 | 7 | mkdir -p /work/fe/tour 8 | rm -rf /work/fe/tour/* 9 | cp tour.tar.gz /work/fe/tour 10 | cd /work/fe/tour && tar -xvf tour.tar.gz -------------------------------------------------------------------------------- /fe/src/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | 20 | -------------------------------------------------------------------------------- /fe/src/api/auth.ts: -------------------------------------------------------------------------------- 1 | import httpRequest, { ResponseData } from './index' 2 | import { AxiosPromise } from 'axios' 3 | 4 | export const login = ( 5 | phone: string, 6 | pwd: string 7 | ): AxiosPromise => { 8 | return httpRequest.request({ 9 | method: 'post', 10 | url: '/tour/auth/loginForm', 11 | data: { phone, pwd } 12 | }) 13 | } 14 | 15 | export const regist = ( 16 | phone: string, 17 | pwd: string 18 | ): AxiosPromise => { 19 | return httpRequest.request({ 20 | method: 'post', 21 | url: '/tour/auth/registForm', 22 | data: { phone, pwd } 23 | }) 24 | } 25 | 26 | export const getPhoneCode = (phone: string): AxiosPromise => { 27 | return httpRequest.request({ 28 | method: 'post', 29 | url: '/tour/auth/getPhoneCode', 30 | data: { phone } 31 | }) 32 | } 33 | 34 | export const resetPassword = ( 35 | phone: string, 36 | pwd: string 37 | ): AxiosPromise => { 38 | return httpRequest.request({ 39 | method: 'post', 40 | url: '/tour/auth/resetPassword', 41 | data: { phone, pwd } 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /fe/src/api/comment.ts: -------------------------------------------------------------------------------- 1 | import httpRequest, { ResponseData } from './index' 2 | import { AxiosPromise } from 'axios' 3 | 4 | export const getCouponComment = ( 5 | couponId: number 6 | ): AxiosPromise => { 7 | return httpRequest.request({ 8 | method: 'get', 9 | url: '/tour/comment/get', 10 | params: { couponId } 11 | }) 12 | } 13 | 14 | export const publishComment = ( 15 | userId: number, 16 | commentStar: number, 17 | commentContent: string, 18 | couponId: number 19 | ): AxiosPromise => { 20 | return httpRequest.request({ 21 | method: 'post', 22 | url: '/tour/comment/publish', 23 | data: { userId, commentStar, commentContent, couponId } 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /fe/src/api/coupon.ts: -------------------------------------------------------------------------------- 1 | import httpRequest, { ResponseData } from './index' 2 | import { AxiosPromise } from 'axios' 3 | 4 | export const getCouponsList = ( 5 | regionId: number, 6 | classifyId: number, 7 | page: number 8 | ): AxiosPromise => { 9 | return httpRequest.request({ 10 | method: 'get', 11 | url: '/tour/coupon/list', 12 | params: { regionId, classifyId, page } 13 | }) 14 | } 15 | 16 | export const getClassifyList = (): AxiosPromise => { 17 | return httpRequest.request({ 18 | method: 'get', 19 | url: '/tour/classify/list' 20 | }) 21 | } 22 | 23 | export const getRegionList = (): AxiosPromise => { 24 | return httpRequest.request({ 25 | method: 'get', 26 | url: '/tour/region/list' 27 | }) 28 | } 29 | 30 | export const getCouponDetail = (id: number): AxiosPromise => { 31 | return httpRequest.request({ 32 | method: 'get', 33 | url: '/tour/coupon/detail', 34 | params: { id } 35 | }) 36 | } 37 | 38 | export const getCouponRecord = (userId: number): AxiosPromise => { 39 | return httpRequest.request({ 40 | method: 'get', 41 | url: '/tour/coupon/record', 42 | params: { userId } 43 | }) 44 | } 45 | 46 | export const receiveCoupon = ( 47 | couponId: number, 48 | userId: number 49 | ): AxiosPromise => { 50 | return httpRequest.request({ 51 | method: 'post', 52 | url: '/tour/coupon/receive', 53 | data: { couponId, userId } 54 | }) 55 | } 56 | 57 | export const getReceivedCouponList = ( 58 | userId: number, 59 | type: 'union' | 'visa' | 'jinnang' | 'gaodaowu' 60 | ): AxiosPromise => { 61 | return httpRequest.request({ 62 | method: 'get', 63 | url: '/tour/coupon/received', 64 | params: { userId, type } 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /fe/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import HttpRequest from './request' 2 | 3 | export * from './request' 4 | export default new HttpRequest() 5 | -------------------------------------------------------------------------------- /fe/src/api/personal.ts: -------------------------------------------------------------------------------- 1 | import httpRequest, { ResponseData } from './index' 2 | import { AxiosPromise } from 'axios' 3 | 4 | export const getUserInfo = (id: number): AxiosPromise => { 5 | return httpRequest.request({ 6 | method: 'get', 7 | url: '/tour/user/info', 8 | params: { id } 9 | }) 10 | } 11 | 12 | export const changeUserName = ( 13 | userId: number, 14 | userName: string 15 | ): AxiosPromise => { 16 | return httpRequest.request({ 17 | method: 'post', 18 | url: '/tour/user/changeUserName', 19 | data: { userId, userName } 20 | }) 21 | } 22 | 23 | export const changeUserSex = ( 24 | userId: number, 25 | sex: number 26 | ): AxiosPromise => { 27 | return httpRequest.request({ 28 | method: 'post', 29 | url: '/tour/user/changeUserSex', 30 | data: { userId, sex } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /fe/src/api/request.ts: -------------------------------------------------------------------------------- 1 | import axios, { 2 | AxiosInstance, 3 | AxiosRequestConfig, 4 | AxiosPromise, 5 | AxiosResponse 6 | } from 'axios' 7 | 8 | export interface ResponseData { 9 | apiCode: number 10 | data?: any 11 | message: string 12 | } 13 | 14 | const apiBaseUrl = 15 | process.env.NODE_ENV === 'production' 16 | ? 'https://api.0351zhuangxiu.com' 17 | : 'http://localhost:8091' 18 | 19 | class HttpRequest { 20 | constructor(public baseUrl: string = apiBaseUrl) { 21 | this.baseUrl = baseUrl 22 | } 23 | 24 | public request(options: AxiosRequestConfig): AxiosPromise { 25 | const instance: AxiosInstance = axios.create() 26 | options = this.mergeConfig(options) 27 | this.interceptors(instance, options.url) 28 | return instance(options) 29 | } 30 | 31 | private interceptors(instance: AxiosInstance, url?: string) { 32 | // 请求拦截 33 | instance.interceptors.request.use( 34 | (config: AxiosRequestConfig) => { 35 | config.headers['Token'] = '123456' 36 | config.headers['Platform'] = 'h5/1.2.3' 37 | return config 38 | }, 39 | error => Promise.reject(error) 40 | ) 41 | 42 | // 响应拦截 43 | instance.interceptors.response.use( 44 | (res: AxiosResponse) => { 45 | return res 46 | }, 47 | error => Promise.reject(error) 48 | ) 49 | } 50 | 51 | private mergeConfig(options: AxiosRequestConfig): AxiosRequestConfig { 52 | return Object.assign({ baseURL: this.baseUrl }, options) 53 | } 54 | } 55 | 56 | export default HttpRequest 57 | -------------------------------------------------------------------------------- /fe/src/api/wechat.ts: -------------------------------------------------------------------------------- 1 | import httpRequest, { ResponseData } from './index' 2 | import { AxiosPromise } from 'axios' 3 | 4 | export const authorizeUserInfo = ( 5 | code: number | string 6 | ): AxiosPromise => { 7 | return httpRequest.request({ 8 | method: 'post', 9 | url: 'https://api.0351zhuangxiu.com/wechat/auth/authorizeUserInfo', 10 | params: { code } 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /fe/src/components/column-divide/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | 52 | -------------------------------------------------------------------------------- /fe/src/components/count-down/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 85 | 86 | 105 | -------------------------------------------------------------------------------- /fe/src/components/coupon-brief/images/quan_banklogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/components/coupon-brief/images/quan_banklogo.png -------------------------------------------------------------------------------- /fe/src/components/coupon-brief/images/quan_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/components/coupon-brief/images/quan_logo.png -------------------------------------------------------------------------------- /fe/src/components/coupon-brief/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 63 | 64 | 107 | -------------------------------------------------------------------------------- /fe/src/components/coupon-comment/images/notclickstar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/components/coupon-comment/images/notclickstar.png -------------------------------------------------------------------------------- /fe/src/components/coupon-comment/images/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/components/coupon-comment/images/star.png -------------------------------------------------------------------------------- /fe/src/components/coupon-comment/index.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 73 | 74 | 95 | -------------------------------------------------------------------------------- /fe/src/components/coupon-rule/images/flex_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/components/coupon-rule/images/flex_down.png -------------------------------------------------------------------------------- /fe/src/components/coupon-rule/images/flex_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/components/coupon-rule/images/flex_up.png -------------------------------------------------------------------------------- /fe/src/components/coupon-rule/index.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 67 | 68 | 124 | -------------------------------------------------------------------------------- /fe/src/components/footer-nav/images/discountsblue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/components/footer-nav/images/discountsblue.png -------------------------------------------------------------------------------- /fe/src/components/footer-nav/images/discountsgrey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/components/footer-nav/images/discountsgrey.png -------------------------------------------------------------------------------- /fe/src/components/footer-nav/images/mineblue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/components/footer-nav/images/mineblue.png -------------------------------------------------------------------------------- /fe/src/components/footer-nav/images/minegrey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/components/footer-nav/images/minegrey.png -------------------------------------------------------------------------------- /fe/src/components/footer-nav/index.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 64 | 65 | 105 | -------------------------------------------------------------------------------- /fe/src/components/header-explain/goback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/components/header-explain/goback.png -------------------------------------------------------------------------------- /fe/src/components/header-explain/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 39 | 40 | 69 | -------------------------------------------------------------------------------- /fe/src/components/star/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 58 | 59 | 79 | -------------------------------------------------------------------------------- /fe/src/components/upload/images/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/components/upload/images/back.png -------------------------------------------------------------------------------- /fe/src/components/upload/images/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/components/upload/images/delete.png -------------------------------------------------------------------------------- /fe/src/components/upload/images/grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/components/upload/images/grey.png -------------------------------------------------------------------------------- /fe/src/components/upload/images/upload-btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/components/upload/images/upload-btn.png -------------------------------------------------------------------------------- /fe/src/components/upload/index.ts: -------------------------------------------------------------------------------- 1 | import Upload from './upload.vue'; 2 | export default Upload; -------------------------------------------------------------------------------- /fe/src/components/upload/index.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 209 | 210 | 270 | -------------------------------------------------------------------------------- /fe/src/components/upload/styles/upload.less: -------------------------------------------------------------------------------- 1 | .align-items { 2 | display: flex; 3 | -webkit-box-align: center; 4 | -moz-box-align: center; 5 | -ms-flex-align: center; 6 | -webkit-align-items: center; 7 | align-items: center; 8 | -webkit-box-pack: center; 9 | -moz-box-pack: center; 10 | -ms-flex-pack: center; 11 | -webkit-justify-content: center; 12 | justify-content: center; 13 | } -------------------------------------------------------------------------------- /fe/src/components/upload/upload.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * upload file 3 | * @author zhaoyiming 4 | * @since 2019/09/09 5 | */ 6 | 7 | interface Headers { 8 | [propName: string]: any 9 | } 10 | 11 | interface Data { 12 | [propName: string]: any 13 | } 14 | 15 | interface Options { 16 | action: string 17 | headers: Headers 18 | withCredentials: boolean 19 | fileName: string 20 | data: Data 21 | file: string | Blob 22 | onProgress: (process: number) => void 23 | onSuccess: (res: any) => void, 24 | onError: (res: any) => void, 25 | onComplete: (res: any) => void 26 | } 27 | 28 | function upload (options: Options) { 29 | const xhr = window.XMLHttpRequest 30 | ? new XMLHttpRequest() 31 | : new ActiveXObject('Microsoft.XMLHTTP'); 32 | 33 | if (validateContextAndParams(xhr, options) !== true) { 34 | return; 35 | } 36 | 37 | initUploadOptions(xhr, options); 38 | run(xhr, options); 39 | } 40 | 41 | function validateContextAndParams (xhr: XMLHttpRequest, options: Options) { 42 | if (!xhr.upload) { 43 | return error('Current browsers do not support file upload'); 44 | } 45 | 46 | if (!isPlainObject(options)) { 47 | return error(`The parameter of "upload" function must be an object`); 48 | } 49 | 50 | if (!options.action) { 51 | return error('The options.action is not defined'); 52 | } 53 | 54 | if (!options.file) { 55 | return error('The options.file is not defined'); 56 | } 57 | 58 | return true; 59 | } 60 | 61 | function initUploadOptions (xhr: XMLHttpRequest, options: Options) { 62 | xhr.upload.onprogress = function (ev: ProgressEvent): any { 63 | let percent: number = 0; 64 | if (ev.total > 0) { 65 | percent = ev.loaded / ev.total * 100; 66 | } 67 | 68 | const onProgress = options.onProgress; 69 | isFunction(onProgress) && onProgress(percent); 70 | } 71 | } 72 | 73 | function run (xhr: XMLHttpRequest, options: Options) { 74 | xhr.open('POST', options.action, true); 75 | 76 | withCredentials(xhr, options.withCredentials); 77 | setHeaders(xhr, options); 78 | 79 | const formData = setBody(options); 80 | 81 | onError(xhr, options); 82 | onLoad(xhr, options); 83 | send(xhr, formData); 84 | } 85 | 86 | function withCredentials (xhr: XMLHttpRequest, withCredentials: boolean) { 87 | if (withCredentials && 'withCredentials' in xhr) { 88 | xhr.withCredentials = withCredentials; 89 | } 90 | } 91 | 92 | function setHeaders (xhr: XMLHttpRequest, options: Options) { 93 | const headers = options.headers; 94 | 95 | if (!headers) return; 96 | if (!isPlainObject(headers)) throw new Error('The prop of headers must be an object'); 97 | 98 | for (let prop in headers) { 99 | const value = headers[prop]; 100 | if (headers.hasOwnProperty(prop) && value) { 101 | xhr.setRequestHeader(prop, value); 102 | } 103 | } 104 | } 105 | 106 | function setBody (options: Options) { 107 | const formData = new FormData(); 108 | const data = options.data; 109 | const fileName = options.fileName 110 | ? options.fileName 111 | : 'file'; 112 | 113 | if (data) { 114 | Object.keys(data).map(key => { 115 | formData.append(key, data[key]); 116 | }); 117 | } 118 | 119 | formData.append(fileName, options.file); 120 | 121 | return formData; 122 | } 123 | 124 | function send (xhr: XMLHttpRequest, formData: FormData) { 125 | xhr.send(formData); 126 | } 127 | 128 | function onError (xhr: XMLHttpRequest, options: Options) { 129 | xhr.onerror = function error (e) { 130 | getError(options, getBody(xhr)); 131 | } 132 | } 133 | 134 | function onLoad (xhr: XMLHttpRequest, options: Options) { 135 | xhr.onload = function onload () { 136 | const status = xhr.status; 137 | const res = getBody(xhr); 138 | const onComplete = options.onComplete; 139 | 140 | if (status >= 200 && status < 300) { 141 | getSuccess(options, xhr); 142 | } else { 143 | getError(options, res); 144 | } 145 | 146 | isFunction(onComplete) && onComplete(res); 147 | } 148 | } 149 | 150 | function getBody (xhr: XMLHttpRequest) { 151 | const response = xhr.responseText || xhr.response; 152 | try { 153 | return JSON.parse(response); 154 | } catch (e) { 155 | return e; 156 | } 157 | } 158 | 159 | function getSuccess (options: Options, xhr: XMLHttpRequest) { 160 | const onSuccess = options.onSuccess; 161 | isFunction(onSuccess) && onSuccess(getBody(xhr)); 162 | } 163 | 164 | function getError (options: Options, e: XMLHttpRequestResponseType) { 165 | const onError = options.onError; 166 | isFunction(onError) && onError(e); 167 | } 168 | 169 | function isPlainObject (field: any) { 170 | return Object.prototype.toString.call(field) === '[object Object]'; 171 | } 172 | 173 | function isFunction (field: any) { 174 | return Object.prototype.toString.call(field) === '[object Function]'; 175 | } 176 | 177 | function error (msg: string) { 178 | console.error(msg); 179 | } 180 | 181 | export default upload; 182 | -------------------------------------------------------------------------------- /fe/src/libs/test/index.js: -------------------------------------------------------------------------------- 1 | export default function test () { 2 | return 123; 3 | } -------------------------------------------------------------------------------- /fe/src/libs/wx/ajax.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 用于微信相关接口的简易 ajax 封装 3 | */ 4 | 5 | interface AjaxRequest { 6 | method: 'GET' | 'get' | 'POST' | 'post' 7 | url: string 8 | data?: any // post 9 | } 10 | 11 | interface AjaxResponse { 12 | [prop: string]: any 13 | } 14 | 15 | export default async function ajax(options: AjaxRequest): Promise { 16 | return new Promise((resolve, reject) => { 17 | const xhr = new XMLHttpRequest(); 18 | const method = options.method.toUpperCase(); 19 | 20 | if (method === 'GET') { 21 | xhr.open(method, options.url, true); 22 | } 23 | 24 | if (method === 'POST') { 25 | xhr.open(method, options.url, true); 26 | xhr.setRequestHeader('Content-type', 'application/json'); 27 | xhr.send(JSON.stringify(options.data)); 28 | } else { 29 | xhr.send(); 30 | } 31 | 32 | xhr.onreadystatechange = () => { 33 | if (xhr.readyState !== 4 || xhr.status === 0) return; 34 | 35 | const responseData: AjaxResponse = JSON.parse(xhr.response); 36 | 37 | if (xhr.status >= 200 && xhr.status < 300) { 38 | resolve(responseData); 39 | } else { 40 | reject(`request failed with status code ${xhr.status}`); 41 | } 42 | }; 43 | }); 44 | } -------------------------------------------------------------------------------- /fe/src/libs/wx/auth.ts: -------------------------------------------------------------------------------- 1 | import ajax from './ajax'; 2 | 3 | async function wxAuth (redirectURI: string) { 4 | const result = await ajax({ 5 | method: 'POST', 6 | url: 'https://api.0351zhuangxiu.com/wechat/auth/authorizeCode', 7 | data: { 8 | redirectURI 9 | } 10 | }); 11 | 12 | window.location.href = result.url; 13 | } 14 | 15 | export default wxAuth; -------------------------------------------------------------------------------- /fe/src/libs/wx/index.ts: -------------------------------------------------------------------------------- 1 | import ajax from './ajax'; 2 | 3 | async function wechat() { 4 | const ua: string = navigator.userAgent.toLowerCase(); 5 | const reg: RegExp = /MicroMessenger/i; 6 | const matchedResult: RegExpMatchArray | null = ua.match(reg); 7 | 8 | if (matchedResult === null || matchedResult[0] !== 'micromessenger') { 9 | return; 10 | } 11 | 12 | const config = await ajax({ 13 | method: 'GET', 14 | url: 'https://api.0351zhuangxiu.com/wechat/auth/signature' 15 | }); 16 | 17 | wx.config({ 18 | debug: false, 19 | appId: config.appId, 20 | timestamp: config.timestamp, 21 | nonceStr: config.nonceStr, 22 | signature: config.signature, 23 | jsApiList: [ 24 | 'checkJsApi', 25 | 'onMenuShareTimeline', 26 | 'onMenuShareAppMessage', 27 | 'onMenuShareQQ', 28 | 'onMenuShareQZone', 29 | 'chooseImage', 30 | 'uploadImage', 31 | 'downloadImage', 32 | 'scanQRCode', 33 | 'getLocation' 34 | ] 35 | }); 36 | 37 | wx.ready(() => { 38 | wx.checkJsApi({ 39 | jsApiList: [ 40 | 'onMenuShareTimeline', 41 | 'onMenuShareAppMessage', 42 | 'onMenuShareQQ', 43 | 'onMenuShareQZone', 44 | 'chooseImage' 45 | ], 46 | success (res: any) { 47 | console.log(res); 48 | } 49 | }); 50 | 51 | const shareOption = { 52 | title: '欢迎来到锦囊团官网', 53 | desc: '全世界各地的优惠券免费拿哦', 54 | link: window.location.href, 55 | imgUrl: 'https://web.0351zhuangxiu.com/tour/static/images/jnt.png' 56 | }; 57 | 58 | wx.onMenuShareTimeline(shareOption); 59 | wx.onMenuShareAppMessage(shareOption); 60 | wx.getLocation({ 61 | type: 'wgs84', // 默认为wgs84的gps坐标,如果要返回直接给openLocation用的火星坐标,可传入'gcj02' 62 | success: function (res) { 63 | var latitude = res.latitude; // 纬度,浮点数,范围为90 ~ -90 64 | var longitude = res.longitude; // 经度,浮点数,范围为180 ~ -180。 65 | } 66 | }); 67 | }); 68 | } 69 | 70 | export default wechat; -------------------------------------------------------------------------------- /fe/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | // import { Toast, Dialog, Loading } from 'vant' 6 | 7 | const app = createApp(App) 8 | 9 | app.use(router) 10 | app.use(store) 11 | 12 | // app.use(Toast) 13 | // app.use(Dialog) 14 | // app.use(Loading) 15 | 16 | app.mount('#app') 17 | 18 | // Vue.prototype.uploadFile = process.env.NODE_ENV === 'development' 19 | // ? 'http://localhost:8091/tour/user/changeUserHeadpic' 20 | // : 'https://api.0351zhuangxiu.com/tour/user/changeUserHeadpic' 21 | -------------------------------------------------------------------------------- /fe/src/mixins/directive.ts: -------------------------------------------------------------------------------- 1 | let scrollTop = 0 2 | 3 | export const focus = { 4 | mounted: (el: HTMLAnchorElement) => { 5 | el.onfocus = (ev: any) => { 6 | scrollTop = window.pageYOffset 7 | } 8 | } 9 | } 10 | 11 | export const blur = { 12 | mounted: (el: HTMLAnchorElement) => { 13 | el.onblur = (ev: Event) => { 14 | window.scrollTo(0, scrollTop) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /fe/src/pages/account/get-phone-code.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 100 | 101 | 116 | -------------------------------------------------------------------------------- /fe/src/pages/account/images/account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/account/images/account.png -------------------------------------------------------------------------------- /fe/src/pages/account/images/flow1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/account/images/flow1.png -------------------------------------------------------------------------------- /fe/src/pages/account/images/flow2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/account/images/flow2.png -------------------------------------------------------------------------------- /fe/src/pages/account/images/password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/account/images/password.png -------------------------------------------------------------------------------- /fe/src/pages/account/images/phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/account/images/phone.png -------------------------------------------------------------------------------- /fe/src/pages/account/images/verify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/account/images/verify.png -------------------------------------------------------------------------------- /fe/src/pages/account/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 55 | 56 | 85 | -------------------------------------------------------------------------------- /fe/src/pages/account/login.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 96 | 97 | 100 | -------------------------------------------------------------------------------- /fe/src/pages/account/regist.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 112 | 113 | 116 | -------------------------------------------------------------------------------- /fe/src/pages/account/reset-password.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 121 | 122 | 137 | -------------------------------------------------------------------------------- /fe/src/pages/account/tour-app-account.less: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /* 3 | * 登录、注册页、找回密码等表单页面样式 4 | * @author: zhaoyiming 5 | * @since : 2017/9/4 6 | */ 7 | 8 | .account-container { 9 | width : 84%; 10 | padding: 20px 8% 0 8%; 11 | } 12 | 13 | .account-container-form p { 14 | position : relative; 15 | margin-bottom: 15px; 16 | } 17 | 18 | .phone, 19 | .apiCode, 20 | .pwd, 21 | .account { 22 | height : 45px; 23 | border-radius: 3px; 24 | font-size : 14px; 25 | color : #4d4d4d; 26 | } 27 | 28 | .phone-ico, 29 | .apiCode-ico, 30 | .phone-ico, 31 | .pwd-ico { 32 | display : block; 33 | position : absolute; 34 | height : 100%; 35 | background-size: 30px 30px; 36 | } 37 | 38 | .phone-ico { 39 | background-image : url("./images/phone.png"); 40 | background-repeat : no-repeat; 41 | background-position: 0 7px; 42 | } 43 | 44 | .apiCode-ico { 45 | background-image : url("./images/verify.png"); 46 | background-repeat : no-repeat; 47 | background-position: 0 7px; 48 | } 49 | 50 | .phone-ico { 51 | background-image : url("./images/account.png"); 52 | background-repeat : no-repeat; 53 | background-position: 0 7px; 54 | } 55 | 56 | .pwd-ico { 57 | background-image : url("./images/password.png"); 58 | background-repeat : no-repeat; 59 | background-position: 0 7px; 60 | } 61 | 62 | .account-btn { 63 | display : block; 64 | width : 100%; 65 | height : 45px; 66 | margin : 0 0 20px 0; 67 | text-align : center; 68 | line-height : 45px; 69 | border-radius : 25px; 70 | background-color: #2577e3; 71 | color : #fff; 72 | font-size : 15px; 73 | } 74 | 75 | .unable { 76 | display : block; 77 | text-align: right; 78 | color : #2577e3; 79 | font-size : 12px; 80 | } 81 | 82 | .phone-prompt { 83 | display : block; 84 | text-align: right; 85 | color : #afafaf; 86 | font-size : 12px; 87 | } 88 | 89 | @media only screen and (max-width: 319px) { 90 | 91 | .phone-ico, 92 | .apiCode-ico, 93 | .pwd-ico, 94 | .phone-ico { 95 | width: 11%; 96 | } 97 | 98 | .phone, 99 | .apiCode, 100 | .pwd, 101 | .account { 102 | width : 89%; 103 | padding-left: 11%; 104 | } 105 | } 106 | 107 | @media only screen and (min-width: 320px) and (max-width: 374px) { 108 | 109 | .phone-ico, 110 | .apiCode-ico, 111 | .pwd-ico, 112 | .phone-ico { 113 | width: 11%; 114 | } 115 | 116 | .phone, 117 | .apiCode, 118 | .pwd, 119 | .account { 120 | width : 89%; 121 | padding-left: 11%; 122 | } 123 | } 124 | 125 | @media only screen and (min-width: 375px) and (max-width: 413px) { 126 | 127 | .phone-ico, 128 | .apiCode-ico, 129 | .pwd-ico, 130 | .phone-ico { 131 | width: 10%; 132 | } 133 | 134 | .phone, 135 | .apiCode, 136 | .pwd, 137 | .account { 138 | width : 90%; 139 | padding-left: 10%; 140 | } 141 | } 142 | 143 | @media only screen and (min-width: 414px) and (max-width: 480px) { 144 | 145 | .phone-ico, 146 | .apiCode-ico, 147 | .pwd-ico, 148 | .phone-ico { 149 | width: 9%; 150 | } 151 | 152 | .phone, 153 | .apiCode, 154 | .pwd, 155 | .account { 156 | width : 91%; 157 | padding-left: 9%; 158 | } 159 | } 160 | 161 | @media only screen and (min-width: 480px) and (max-width: 767px) { 162 | 163 | .phone-ico, 164 | .apiCode-ico, 165 | .pwd-ico, 166 | .phone-ico { 167 | width: 7%; 168 | } 169 | 170 | .phone, 171 | .apiCode, 172 | .pwd, 173 | .account { 174 | width : 93%; 175 | padding-left: 7%; 176 | } 177 | } 178 | 179 | @media only screen and (min-width: 768px) { 180 | 181 | .phone-ico, 182 | .apiCode-ico, 183 | .pwd-ico, 184 | .phone-ico { 185 | width: 6%; 186 | } 187 | 188 | .phone, 189 | .apiCode, 190 | .pwd, 191 | .account { 192 | width : 94%; 193 | padding-left: 6%; 194 | } 195 | } -------------------------------------------------------------------------------- /fe/src/pages/get-coupon/images/lingqu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/get-coupon/images/lingqu.png -------------------------------------------------------------------------------- /fe/src/pages/get-coupon/images/pay_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/get-coupon/images/pay_line.png -------------------------------------------------------------------------------- /fe/src/pages/get-coupon/images/quan_cards.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/get-coupon/images/quan_cards.png -------------------------------------------------------------------------------- /fe/src/pages/get-coupon/images/quan_er.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/get-coupon/images/quan_er.png -------------------------------------------------------------------------------- /fe/src/pages/get-coupon/images/quan_get.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/get-coupon/images/quan_get.png -------------------------------------------------------------------------------- /fe/src/pages/get-coupon/images/quan_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/get-coupon/images/quan_line.png -------------------------------------------------------------------------------- /fe/src/pages/get-coupon/images/quan_plogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/get-coupon/images/quan_plogo.png -------------------------------------------------------------------------------- /fe/src/pages/home.vue: -------------------------------------------------------------------------------- 1 | 121 | 122 | 277 | 278 | 364 | -------------------------------------------------------------------------------- /fe/src/pages/personal/change-headpic.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 59 | 60 | 92 | -------------------------------------------------------------------------------- /fe/src/pages/personal/change-user-name.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 82 | 83 | 97 | -------------------------------------------------------------------------------- /fe/src/pages/personal/change-user-sex.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 87 | 88 | 111 | -------------------------------------------------------------------------------- /fe/src/pages/personal/images/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/personal/images/check.png -------------------------------------------------------------------------------- /fe/src/pages/personal/images/flex_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/personal/images/flex_down.png -------------------------------------------------------------------------------- /fe/src/pages/personal/images/flex_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/personal/images/flex_up.png -------------------------------------------------------------------------------- /fe/src/pages/personal/images/head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/personal/images/head.png -------------------------------------------------------------------------------- /fe/src/pages/personal/images/install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/personal/images/install.png -------------------------------------------------------------------------------- /fe/src/pages/personal/images/jinnangtuan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/personal/images/jinnangtuan.png -------------------------------------------------------------------------------- /fe/src/pages/personal/images/list_dutyfree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/personal/images/list_dutyfree.png -------------------------------------------------------------------------------- /fe/src/pages/personal/images/list_unionpay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/personal/images/list_unionpay.png -------------------------------------------------------------------------------- /fe/src/pages/personal/images/list_visa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/personal/images/list_visa.png -------------------------------------------------------------------------------- /fe/src/pages/personal/images/minebg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/personal/images/minebg.png -------------------------------------------------------------------------------- /fe/src/pages/personal/images/personal_head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/personal/images/personal_head.png -------------------------------------------------------------------------------- /fe/src/pages/personal/images/personal_headbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/personal/images/personal_headbg.png -------------------------------------------------------------------------------- /fe/src/pages/personal/images/phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/personal/images/phone.png -------------------------------------------------------------------------------- /fe/src/pages/personal/images/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/personal/images/right.png -------------------------------------------------------------------------------- /fe/src/pages/personal/images/sex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/personal/images/sex.png -------------------------------------------------------------------------------- /fe/src/pages/personal/images/username.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/pages/personal/images/username.png -------------------------------------------------------------------------------- /fe/src/pages/personal/personal-edit.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 132 | 133 | 204 | -------------------------------------------------------------------------------- /fe/src/pages/personal/tour-app-personal.less: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | /* 4 | * Description: 个人中心相关样式 5 | * User: zhaoyiming 6 | * Date: 2017/9/18 7 | */ 8 | 9 | .personal-msg-wraper { 10 | position: relative; 11 | bottom: 30px; 12 | height: 145px; 13 | background-image: url("./images/minebg.png"); 14 | background-repeat: no-repeat; 15 | background-position: center top; 16 | background-size: 100% 100%; 17 | .head { 18 | position: absolute; 19 | left: 50%; 20 | top: 38%; 21 | margin-left: -40px; 22 | width: 80px; 23 | height: 80px; 24 | img { 25 | border-radius: 50%; 26 | } 27 | } 28 | .personal-edit-head { 29 | position: absolute; 30 | left: 50%; 31 | top: 16%; 32 | margin-left: -60px; 33 | width: 119px; 34 | height: 119px; 35 | img { 36 | border-radius: 50%; 37 | } 38 | } 39 | .phone { 40 | position: absolute; 41 | left: 50%; 42 | top: 95%; 43 | margin-left: -68px; 44 | width: 136px; 45 | height: 30px; 46 | .phone-ico { 47 | vertical-align: middle; 48 | } 49 | .phone-num{ 50 | display: inline-block; 51 | vertical-align: middle; 52 | line-height: 30px; 53 | font-size: 15px; 54 | } 55 | } 56 | .setting { 57 | position: absolute; 58 | right: 0; 59 | top: 25%; 60 | right: 10px; 61 | display: inline-block; 62 | width: 17px; 63 | height: 17px; 64 | background-image: url("./images/install.png"); 65 | background-repeat: no-repeat; 66 | background-position: center top; 67 | background-size: 17px 17px; 68 | } 69 | 70 | } 71 | 72 | .personal-edit-msg-wraper { 73 | position: relative; 74 | top: -5px; 75 | bottom: 30px; 76 | height: 186px; 77 | background-image: url("./images/personal_headbg.png"); 78 | background-repeat: no-repeat; 79 | background-position: center top; 80 | background-size: 100% 100%; 81 | } -------------------------------------------------------------------------------- /fe/src/pages/wechat/auth.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 31 | -------------------------------------------------------------------------------- /fe/src/pages/wechat/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /fe/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | 3 | import routes from './routes' 4 | 5 | const router = createRouter({ 6 | history: createWebHistory('/tour/'), 7 | routes 8 | }) 9 | 10 | router.afterEach((to, from) => { 11 | document.title = '锦囊团' 12 | }) 13 | 14 | export default router 15 | -------------------------------------------------------------------------------- /fe/src/router/routes.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | 3 | const routes: RouteRecordRaw[] = [ 4 | { 5 | path: '/', 6 | redirect: '/home' 7 | }, 8 | { 9 | path: '/account', 10 | name: 'AccountIndex', 11 | component: () => import('@/pages/account/index.vue'), 12 | children: [ 13 | { 14 | path: 'login', 15 | name: 'Login', 16 | component: () => import('@/pages/account/login.vue') 17 | }, 18 | { 19 | path: 'regist', 20 | name: 'Regist', 21 | component: () => import('@/pages/account/regist.vue') 22 | }, 23 | { 24 | path: 'get-phone-code', 25 | name: 'GetPhoneCode', 26 | component: () => import('@/pages/account/get-phone-code.vue') 27 | }, 28 | { 29 | path: 'reset-password', 30 | name: 'ResetPassword', 31 | component: () => import('@/pages/account/reset-password.vue') 32 | } 33 | ] 34 | }, 35 | { 36 | path: '/home', 37 | name: 'Home', 38 | component: () => import('@/pages/home.vue'), 39 | meta: { 40 | keepAlive: true 41 | } 42 | }, 43 | { 44 | path: '/get-coupon', 45 | name: 'GetCoupon', 46 | component: () => import('@/pages/get-coupon/index.vue') 47 | }, 48 | { 49 | path: '/personal', 50 | name: 'Personal', 51 | component: () => import('@/pages/personal/index.vue') 52 | }, 53 | { 54 | path: '/personal-edit', 55 | name: 'PersonEdit', 56 | component: () => import('@/pages/personal/personal-edit.vue') 57 | }, 58 | { 59 | path: '/change-user-thumb', 60 | name: 'ChangeUserThumb', 61 | component: () => import('@/pages/personal/change-headpic.vue') 62 | }, 63 | { 64 | path: '/change-user-name', 65 | name: 'ChangeUserName', 66 | component: () => import('@/pages/personal/change-user-name.vue') 67 | }, 68 | { 69 | path: '/change-user-sex', 70 | name: 'ChangeUserSex', 71 | component: () => import('@/pages/personal/change-user-sex.vue') 72 | }, 73 | { 74 | path: '/wechat', 75 | name: 'Wechat', 76 | component: () => import('@/pages/wechat/index.vue') 77 | }, 78 | { 79 | path: '/auth', 80 | name: 'Auth', 81 | component: () => import('@/pages/wechat/auth.vue') 82 | } 83 | ] 84 | 85 | export default routes 86 | -------------------------------------------------------------------------------- /fe/src/static/css/tour-app-base.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | /** 4 | * Description: 项目公共样式 5 | * User: zhaoyiming 6 | * Date: 2017/08/16 7 | */ 8 | 9 | body, 10 | ul, 11 | li, 12 | p, 13 | a, 14 | h1, 15 | h2, 16 | h3, 17 | h4, 18 | h5, 19 | h6, 20 | form, 21 | fieldset, 22 | table, 23 | td, 24 | div, 25 | dl, 26 | dt, 27 | dd, 28 | input, 29 | img { 30 | margin: 0; 31 | padding: 0; 32 | border: 0; 33 | outline: none; 34 | font-size: 100%; 35 | font: inherit; 36 | vertical-align: baseline; 37 | } 38 | 39 | html, 40 | body { 41 | width: 100%; 42 | height: 100%; 43 | } 44 | 45 | ol, 46 | ul { 47 | list-style: none; 48 | } 49 | 50 | body { 51 | line-height: 1; 52 | font: 400 14px/1.5 "PingFang SC", "Microsoft YaHei", Arial, Helvetica, sans-serif; 53 | } 54 | 55 | input, textarea { 56 | border:none 0; 57 | outline:none; 58 | -webkit-appearance:none; 59 | } 60 | 61 | li { 62 | list-style: none; 63 | } 64 | 65 | a { 66 | text-decoration: none; 67 | } 68 | 69 | table { 70 | border-collapse: collapse; 71 | border-spacing: 0; 72 | } 73 | 74 | .clearfix:after { 75 | display: block; 76 | height: 0; 77 | clear: both; 78 | content: ""; 79 | } 80 | 81 | .dis-none{ 82 | display: none; 83 | } 84 | 85 | .dis-block { 86 | display: block; 87 | } 88 | 89 | .yijianbtn { 90 | display: block; 91 | width: 163px; 92 | height: 59px; 93 | margin: 0 auto; 94 | } 95 | 96 | .wraper { 97 | width: 100%; 98 | min-width: 320px; 99 | max-width: 640px; 100 | margin: 0 auto; 101 | padding: 45px 0 80px 0; 102 | overflow: hidden; 103 | } 104 | 105 | .fixed-header { 106 | position: fixed; 107 | top: 0; 108 | z-index: 10001; 109 | width: 100%; 110 | min-width: 320px; 111 | max-width: 640px; 112 | background-color: #fff; 113 | } 114 | 115 | .fixed-header-explain { 116 | position: relative; 117 | height: 45px; 118 | text-align: center; 119 | line-height: 45px; 120 | font-size: 18px; 121 | color: #383838; 122 | } 123 | 124 | .fixed-header-explain-goback { 125 | position: absolute; 126 | top: 7px; 127 | left: 10px; 128 | display: block; 129 | width: 21px; 130 | height: 31px; 131 | background: url("../images/goback.png") no-repeat left center; 132 | background-size: 12px 18px; 133 | } 134 | 135 | .nav { 136 | position: absolute; 137 | right: 10px; 138 | top: 16px; 139 | width: 20px; 140 | height: 20px; 141 | } 142 | 143 | .nav i { 144 | display: block; 145 | width: 100%; 146 | height: 2px; 147 | border-radius: 5px; 148 | background-color: #fff; 149 | margin-bottom: 4px; 150 | color: #fff; 151 | } 152 | 153 | .bg-f1f4fd { 154 | background: #f1f4fd; 155 | } 156 | 157 | .distance-wraper { 158 | margin: 0 10px 15px 10px; 159 | } 160 | 161 | /* 162 | * 遮罩层 163 | */ 164 | .mask { 165 | position: fixed; 166 | bottom: 0; 167 | right: 0; 168 | z-index: 10000; 169 | width: 100%; 170 | height: 100%; 171 | background-color: rgba(0, 0, 0, 0.3); 172 | } 173 | 174 | .wechat { 175 | margin: 0 auto; 176 | text-align: center; 177 | } -------------------------------------------------------------------------------- /fe/src/static/images/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/static/images/check.png -------------------------------------------------------------------------------- /fe/src/static/images/circlered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/static/images/circlered.png -------------------------------------------------------------------------------- /fe/src/static/images/discountbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/static/images/discountbg.png -------------------------------------------------------------------------------- /fe/src/static/images/downarrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/static/images/downarrows.png -------------------------------------------------------------------------------- /fe/src/static/images/goback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/static/images/goback.png -------------------------------------------------------------------------------- /fe/src/static/images/jnt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/static/images/jnt.png -------------------------------------------------------------------------------- /fe/src/static/images/littlebtn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/static/images/littlebtn.png -------------------------------------------------------------------------------- /fe/src/static/images/pastduebg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/static/images/pastduebg.png -------------------------------------------------------------------------------- /fe/src/static/images/pastduebtn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/static/images/pastduebtn.png -------------------------------------------------------------------------------- /fe/src/static/images/taizilogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/static/images/taizilogo.png -------------------------------------------------------------------------------- /fe/src/static/images/usebg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/static/images/usebg.png -------------------------------------------------------------------------------- /fe/src/static/images/usebtn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/fe/src/static/images/usebtn.png -------------------------------------------------------------------------------- /fe/src/static/js/cookie.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Description: JS操作cookie一系列方法封装 3 | * User: zhaoyiming 4 | * Date: 2017/10/5 5 | * License: MIT , https://github.com/zhaoyiming0803/cookie 6 | */ 7 | 8 | ;(function (global, oDoc, factory) { 9 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(oDoc) : 10 | typeof define === 'function' && define.amd ? define([], function () {return factory(oDoc);}) : 11 | (global.cookie = factory(oDoc)); 12 | })(this, document, function (oDoc) { 13 | 'use strict'; 14 | 15 | function Cookie () {} 16 | 17 | Cookie.prototype.get = function (name) { 18 | var cookieStr = oDoc.cookie, 19 | cookieArry = cookieStr.split(';'), 20 | len = cookieArry.length, 21 | cookieObj = {}, 22 | i, tmpArry; 23 | 24 | for (i = 0; i < len; i += 1) { 25 | tmpArry = cookieArry[i].replace(/\s/g, '').split('='); 26 | cookieObj[tmpArry[0]] = tmpArry[1]; 27 | } 28 | 29 | return cookieObj[name]; 30 | }; 31 | 32 | Cookie.prototype.set = function (opt) { 33 | var host = global.location.host, 34 | name = opt.name, 35 | value = opt.value, 36 | expires = opt.expires ? opt.expires : 0, 37 | path = opt.path ? opt.path : '/', 38 | domain = opt.domain ? opt.domain : host.substr(0, host.indexOf(':')); 39 | 40 | if (!name) { 41 | alert('请设置cookie名!'); 42 | return false; 43 | } 44 | 45 | oDoc.cookie = name + '=' + value + '; expires=' + expires + '; path=' + path + ';domain=' + domain; 46 | }; 47 | 48 | Cookie.prototype.unset = function (name) { 49 | var cookieDate = new Date(); 50 | cookieDate.setTime(cookieDate.getTime()-1); 51 | this.set({name: name, value: '', expires: cookieDate.toGMTString()}); 52 | }; 53 | 54 | var cookie = new Cookie(); 55 | 56 | return cookie; 57 | }); 58 | -------------------------------------------------------------------------------- /fe/src/static/js/iframefileupload.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Description: iframefileupload.js通过原生JS实现,用最少的代码库依赖实现页面无刷新上传文件的同时也可以向后端传递json数据等。 3 | * User: zhaoyiming 4 | * Date: 2017/08/15 5 | * License: Apache2.0 , https://github.com/zhaoyiming0803/iframeFileUpload 6 | */ 7 | 8 | ;(function (global, oDoc, factory) { 9 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(oDoc) : 10 | typeof define === 'function' && define.amd ? define([], function () {return factory(oDoc);}) : 11 | (global.iframeFileUpload = factory(oDoc)); 12 | })(this, document, function (oDoc) { 13 | 'use strict'; 14 | 15 | function strToDom (str) { 16 | var oDiv = oDoc.createElement("div"); 17 | oDiv.innerHTML = str; 18 | return oDiv.childNodes[0]; 19 | } 20 | 21 | function createUploadIframe (oDoc, oBody, id) { 22 | var iframeId = 'iframe' + id, 23 | iframeHtml = ''; 24 | oBody.appendChild(strToDom(iframeHtml)); 25 | return oDoc.querySelector('#' + iframeId); 26 | } 27 | 28 | function createUploadForm (oDoc, oBody, url, data, id) { 29 | var formId = 'form' + id, 30 | tmpInpt = null, 31 | formHtml = ''; 37 | 38 | oBody.appendChild(strToDom(formHtml)); 39 | return oDoc.querySelector('#' + formId); 40 | } 41 | 42 | // 从后端获取到的数据 43 | function getData (iframe) { 44 | return iframe.contentWindow.document.body.innerText || iframe.contentDocument.body.innerText; 45 | } 46 | 47 | function IframeFileUpload (opt) { 48 | this.opt = opt; 49 | } 50 | 51 | IframeFileUpload.prototype.init = function () { 52 | var opt = this.opt, 53 | _url = opt.url, // 后端url 54 | _elementId = typeof opt.elementId === 'string' ? [opt.elementId] : opt.elementId ? opt.elementId : false, // 上传表单的id数组集合,例:['file1', 'file2'] 55 | _elementIdLen = _elementId ? _elementId.length : 0, 56 | _data = opt.data, // 发送到后端的数据 57 | _success = opt.success, // 成功回调 58 | _error = opt.error, // 失败回调 59 | oBody = oDoc.body, 60 | id = new Date().getTime(), 61 | iframe = createUploadIframe(oDoc, oBody, id), 62 | form = createUploadForm(oDoc, oBody, _url, _data, id), 63 | frag = null, 64 | tmpNode = null, 65 | oldNode = null; 66 | 67 | if (_elementIdLen) { 68 | frag = oDoc.createDocumentFragment(); 69 | for (var i = 0; i < _elementIdLen; i += 1) { 70 | oldNode = oDoc.querySelector('#' + _elementId[i]); 71 | tmpNode = oldNode.cloneNode(true); 72 | 73 | // clone方法不能拷贝事件,所以需要给新node重新绑定change事件,方便下次执行 74 | tmpNode.addEventListener('change', iframeFileUpload.bind(this, this.opt)); 75 | 76 | oldNode.parentNode.insertBefore(tmpNode, oldNode); 77 | frag.appendChild(oldNode); 78 | } 79 | form.appendChild(frag); 80 | form.submit(); 81 | frag = null; 82 | } 83 | 84 | iframe.onload = function () { 85 | try { 86 | _success(getData(iframe).replace(/<[^>]+>/g,"")); 87 | } catch (e) { 88 | _error(e); 89 | } 90 | 91 | oBody.removeChild(oDoc.querySelector('#iframe' + id)); 92 | oBody.removeChild(oDoc.querySelector('#form' + id)); 93 | 94 | iframe = form = opt = null; 95 | }; 96 | }; 97 | 98 | function iframeFileUpload (opt) { 99 | new IframeFileUpload(opt).init(); 100 | } 101 | 102 | return iframeFileUpload; 103 | }); 104 | -------------------------------------------------------------------------------- /fe/src/static/js/validatefileupload.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Description : 文件上传校验 3 | * User : zhaoyiming 4 | * Date : 2017/07/17 5 | * License: Apache2.0 ,https://github.com/zhaoyiming0803/validateFileUpload 6 | */ 7 | 8 | ;(function (global, oDoc, factory) { 9 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(oDoc) : 10 | typeof define === 'function' && define.amd ? define([], function () {return factory(oDoc);}) : 11 | (global.validateFileUpload = factory(oDoc)); 12 | })(this, document, function (oDoc) { 13 | 'use strict'; 14 | 15 | // 是否是一个function 16 | function isFunction (fn) { 17 | return Object.prototype.toString.call(fn) === '[object Function]' ? fn : function (res) {return res;}; 18 | } 19 | 20 | // 校验各类上传文件大小是否符合规范 21 | function validateSize (_this, file) { 22 | if (file === undefined) { 23 | return false; 24 | } 25 | if(_this.maxSize * Math.pow(1024, 2) < file.size){ 26 | alert('文件最大不能超过' + _this.maxSize + 'M'); 27 | return false; 28 | } 29 | return true; 30 | } 31 | 32 | // 校验各类上传文件格式是否符合规范 33 | function validateType (file, fileType) { 34 | if (file === undefined) { 35 | return false; 36 | } 37 | 38 | var fileTypeLen = fileType.length, 39 | isValidated = false, 40 | fileName = file.name, 41 | suffix = fileName.substr(fileName.indexOf('.') + 1), 42 | i = 0; 43 | 44 | for (; i < fileTypeLen; i++) { 45 | if (suffix.toLowerCase() === fileType[i]) { 46 | isValidated = true; 47 | break; 48 | } 49 | } 50 | 51 | if (!isValidated) { 52 | return false; 53 | } 54 | 55 | return true; 56 | } 57 | 58 | // 在html中显示上传的文件,例如图片 59 | function showSource (_this, file) { 60 | var showEle = _this.showEle; 61 | if( window.FileReader ) { 62 | var fr = new FileReader(); 63 | fr.onloadend = function (e) { 64 | showEle.src = e.target.result; 65 | }; 66 | fr.readAsDataURL( file ); 67 | showEle.style.display = 'block'; 68 | } 69 | } 70 | 71 | // 一系列校验 72 | function validateFile (_this, source, type) { 73 | if (!_this.inptEle) { 74 | alert('请在配置项中指定本地的上传表单!'); 75 | return false; 76 | } 77 | 78 | if (!type) { 79 | return true; 80 | } 81 | 82 | var file = source.files[0]; 83 | 84 | var validateSizeResult = validateSize(_this, file); 85 | if (!validateSizeResult) { 86 | _this.inptEle.value = ''; 87 | return false ; 88 | } 89 | 90 | var validateTypeResult = validateType(file, type); 91 | if (!validateTypeResult) { 92 | _this.inptEle.value = ''; 93 | return false; 94 | } 95 | 96 | if (_this.showEle) { 97 | showSource(_this, file); 98 | } 99 | 100 | return true; 101 | } 102 | 103 | // 校验类 104 | function ValidateFileUpload (opt) { 105 | var fileType = opt.fileType, 106 | maxSize = opt.maxSize, 107 | showEle = opt.showEle, 108 | inptEle = opt.inptEle, 109 | success = opt.success, 110 | error = opt.error; 111 | 112 | this.fileType = fileType ? fileType : null; // 允许上传到文件类型,数组['jpg', 'jpeg', 'png', 'gif', 'bmp', 'docx', 'xls', 'pptx', 'txt', 'mp4', 'mp3'] 113 | this.maxSize = maxSize ? maxSize : 100; // 默认允许上传最大2M的文件 114 | this.showEle = showEle ? oDoc.querySelector('#' + showEle) : null; // 默认在html中显示上传文件的dom,一般用于image 115 | this.inptEle = oDoc.querySelector('#' + inptEle); // 上传文件input表单 116 | this.success = isFunction(success); // 成功时回调 117 | this.error = isFunction(error); // 错误时回调 118 | } 119 | 120 | ValidateFileUpload.prototype.init = function (source) { 121 | return validateFile(this, source, this.fileType) ? true : false; 122 | } 123 | 124 | // 实例化类,并返回校验结果 125 | function validateFileUpload (opt) { 126 | var validateObj = new ValidateFileUpload(opt), 127 | validateRes = validateObj.init(validateObj.inptEle); 128 | validateRes ? validateObj.success(true) : validateObj.error(false); 129 | } 130 | 131 | return validateFileUpload; 132 | }); 133 | -------------------------------------------------------------------------------- /fe/src/static/less/coupon.less: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | /** 4 | * Description: 优惠券列表公用LESS 5 | * User: zhaoyiming 6 | * Date: 2017/9/29 7 | */ 8 | 9 | .coupon-list-wraper { 10 | a { 11 | display: block; 12 | position: relative; 13 | height: 95px; 14 | margin-bottom: 10px; 15 | background-repeat: no-repeat; 16 | background-position: left top; 17 | &.use-discount-bg { 18 | background-image: url("../images/discountbg.png"); 19 | background-size: 100% 100%; 20 | .shop-active { 21 | position: absolute; 22 | top: 13px; 23 | right: 4px; 24 | height: 70px; 25 | } 26 | } 27 | &.used-bg { 28 | background-image: url("../images/usebg.png"); 29 | background-size: 100% 103%; 30 | } 31 | &.past-bg { 32 | background-image: url("../images/pastduebg.png"); 33 | background-size: 100% 103%; 34 | } 35 | .shop-ico { 36 | position: absolute; 37 | left: 8px; 38 | top: 13px; 39 | width: 105px; 40 | height: 70px; 41 | img { 42 | border: 1px solid #e2e2e2; 43 | width: 100%; 44 | height: 100%; 45 | border-radius: 5px; 46 | } 47 | } 48 | .shop-intro { 49 | position: absolute; 50 | left: 118px; 51 | top: 13px; 52 | width: 100%; 53 | .shop-title { 54 | font-size: 15px; 55 | color: #4d4d4d; 56 | } 57 | .shop-discounts { 58 | font-size: 12px; 59 | color: #b3b3b3; 60 | } 61 | .shop-price span{ 62 | display: inline-block; 63 | height: 100%; 64 | line-height: 22px; 65 | vertical-align: middle; 66 | &.condition { 67 | padding: 0 3px; 68 | background-color: #fff4ec; 69 | font-size: 15px; 70 | color: #f23030; 71 | } 72 | } 73 | } 74 | .shop-active { 75 | position: absolute; 76 | top: 45px; 77 | right: 4px; 78 | height: 70px; 79 | p { 80 | height: 18px; 81 | line-height: 30px; 82 | text-align: center; 83 | font-size: 10px; 84 | font-weight: 500; 85 | color: #f23030; 86 | } 87 | .use-discount { 88 | background-image: url("../images/littlebtn.png"); 89 | } 90 | .used { 91 | background-image: url("../images/usebtn.png"); 92 | } 93 | .past { 94 | background-image: url("../images/pastduebtn.png"); 95 | } 96 | } 97 | .shop-active-canuse { 98 | background-image: url("../images/circlered.png"); 99 | background-repeat: no-repeat; 100 | background-position: center top; 101 | background-size: 47px 35px; 102 | } 103 | } 104 | } 105 | 106 | .use-discount, .used, .past { 107 | display: block; 108 | height: 33px; 109 | margin: 10px auto 0 auto; 110 | background-repeat: no-repeat; 111 | background-position: center top; 112 | background-size: 54px 33px; 113 | } 114 | 115 | @media only screen and (min-width:320px) and (max-width:374px) { 116 | .shop-price { 117 | max-width: 125px; 118 | } 119 | 120 | .shop-active { 121 | width: 50px; 122 | } 123 | 124 | .use-discount, .used, .past { 125 | width: 50px; 126 | height: 30px; 127 | background-size: 50px 33px; 128 | } 129 | } 130 | 131 | @media only screen and (min-width:375px) and (max-width:413px) { 132 | .shop-price { 133 | max-width: 156px; 134 | } 135 | 136 | .shop-active { 137 | width: 60px; 138 | } 139 | } 140 | 141 | @media only screen and (min-width:414px) and (max-width:767px) { 142 | .shop-price { 143 | max-width: 185px; 144 | } 145 | 146 | .shop-active { 147 | width: 65px; 148 | } 149 | } 150 | 151 | @media only screen and (min-width:768px) { 152 | .shop-price { 153 | max-width: 360px; 154 | } 155 | 156 | .shop-active { 157 | width: 107px; 158 | background-size: 60px 45px; 159 | p { 160 | height: 20px; 161 | line-height: 42px; 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /fe/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex' 2 | import app, { StateProps as AppState } from './modules/app' 3 | 4 | export type StateProps = { 5 | app: AppState 6 | } 7 | 8 | export default createStore({ 9 | modules: { 10 | app 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /fe/src/store/modules/app.ts: -------------------------------------------------------------------------------- 1 | export interface StateProps { 2 | regionId: number 3 | regionName: string 4 | classifyId: number 5 | classifyName: string 6 | } 7 | export interface ActionsType { 8 | commit(actionHandler: string, actionObject?: any): void 9 | } 10 | 11 | const state: StateProps = { 12 | regionId: 1, 13 | regionName: '全球', 14 | classifyId: 1, 15 | classifyName: '购物' 16 | } 17 | 18 | const mutations = { 19 | changeRegionId (state: StateProps, countryId: number) { 20 | state.regionId = countryId 21 | }, 22 | 23 | changeRegionName (state: StateProps, countryName: string) { 24 | state.regionName = countryName 25 | }, 26 | 27 | changeClassifyId (state: StateProps, classifyId: number) { 28 | state.classifyId = classifyId 29 | }, 30 | 31 | changeClassifyName (state: StateProps, classifyName: string) { 32 | state.classifyName = classifyName 33 | } 34 | } 35 | 36 | export default { 37 | namespaced: true, 38 | state, 39 | mutations 40 | } 41 | -------------------------------------------------------------------------------- /fe/src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | // 2 | declare const wx: Wechat.Wx 3 | declare module '@/api/*' 4 | declare module '@/mixins/*' 5 | declare module '@/utils/*' 6 | declare module '@/libs/*' 7 | -------------------------------------------------------------------------------- /fe/src/types/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue' 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /fe/src/types/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | const component: DefineComponent< 4 | Record, 5 | Record, 6 | any 7 | > 8 | export default component 9 | } 10 | -------------------------------------------------------------------------------- /fe/src/types/test.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@/libs/test' { 2 | export function test(): number 3 | } 4 | -------------------------------------------------------------------------------- /fe/src/types/wechat.ts: -------------------------------------------------------------------------------- 1 | interface ReturnVoidFunction { 2 | (arg: any): void 3 | } 4 | namespace Wechat { 5 | interface WxOptions { 6 | debug?: boolean // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 7 | appId: string // 公众号的唯一标识 8 | timestamp: string // 生成签名的时间戳 9 | nonceStr: string // 生成签名的随机串 10 | signature: string // 签名 11 | jsApiList: [ 12 | // 根据微信文档(附录2)及自身需要填充 13 | 'checkJsApi', 14 | 'onMenuShareTimeline', 15 | 'onMenuShareAppMessage', 16 | 'onMenuShareQQ', 17 | 'onMenuShareQZone', 18 | 'chooseImage', 19 | 'uploadImage', 20 | 'downloadImage', 21 | 'scanQRCode', 22 | 'getLocation' 23 | ] 24 | } 25 | 26 | interface BaseShareOption { 27 | title: string // 分享标题 28 | desc: string // 分享描述 29 | link: string // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致 30 | imgUrl: string // 分享图标 31 | success?: ReturnVoidFunction 32 | } 33 | 34 | /** 35 | * 分享给微信朋友或QQ好友 36 | */ 37 | interface UpdateAppMessageShareData extends BaseShareOption {} 38 | 39 | /** 40 | * 分享到微信朋友圈或QQ空间 41 | */ 42 | interface UpdateTimelineShareData extends BaseShareOption {} 43 | 44 | /** 45 | * 分享给微信好友 46 | */ 47 | interface OnMenuShareAppMessage extends BaseShareOption { 48 | type?: 'music' | 'video' | 'link' // 不填默认为link 49 | dataUrl?: string // 如果type是music或video,则要提供数据链接,默认为空 50 | } 51 | 52 | /** 53 | * 分享到微信朋友圈 54 | */ 55 | interface OnMenuShareTimeline extends BaseShareOption {} 56 | 57 | /** 58 | * 获取地理位置 59 | */ 60 | interface LocationResponse { 61 | latitude: number // 纬度,浮点数,范围为90 ~ -90 62 | longitude: number // 经度,浮点数,范围为180 ~ -180。 63 | speed: number // 速度,以米/每秒计 64 | accuracy: number // 位置精度 65 | } 66 | interface GetLocation { 67 | type: string 68 | success?: (res: LocationResponse) => void 69 | } 70 | 71 | export interface Wx { 72 | config(options: WxOptions): void 73 | ready(fn: ReturnVoidFunction): void 74 | checkJsApi(options: any): void 75 | updateAppMessageShareData(options: UpdateAppMessageShareData): void 76 | updateTimelineShareData(options: UpdateTimelineShareData): void 77 | onMenuShareTimeline(options: OnMenuShareTimeline): void 78 | onMenuShareAppMessage(options: OnMenuShareAppMessage): void 79 | getLocation(options: GetLocation): void 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /fe/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './validator' 2 | -------------------------------------------------------------------------------- /fe/src/utils/validator.ts: -------------------------------------------------------------------------------- 1 | export function validatePhone(phone: string): boolean { 2 | return /^1(\d{10})$/.test(phone) 3 | } 4 | 5 | export function validatePassword(password: string): boolean { 6 | return /\w{6,}/.test(password) 7 | } 8 | -------------------------------------------------------------------------------- /fe/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": [ 15 | "webpack-env" 16 | ], 17 | "paths": { 18 | "@/*": [ 19 | "src/*" 20 | ] 21 | }, 22 | "lib": [ 23 | "esnext", 24 | "dom", 25 | "dom.iterable", 26 | "scripthost" 27 | ] 28 | }, 29 | "include": [ 30 | "src/**/*.ts", 31 | "src/**/*.tsx", 32 | "src/**/*.vue", 33 | "tests/**/*.ts", 34 | "tests/**/*.tsx" 35 | ], 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /fe/vue.config.js: -------------------------------------------------------------------------------- 1 | const webpackBaseConfig = require('./build/webpack.base.config'); 2 | const webpackDevConfig = require('./build/webpack.dev.config'); 3 | const webpackProdConfig = require('./build/webpack.prod.config'); 4 | 5 | const configure = { 6 | development: config => webpackDevConfig(config), 7 | production: config => webpackProdConfig(config) 8 | } 9 | 10 | module.exports = { 11 | parallel: false, 12 | outputDir: 'dist', 13 | publicPath: process.env.NODE_ENV === 'production' 14 | ? '/tour/' 15 | : '/', 16 | configureWebpack: config => configure[process.env.NODE_ENV](config), 17 | chainWebpack: webpackBaseConfig, 18 | // lintOnSave: false // @vue/cli-plugin-eslint 19 | }; -------------------------------------------------------------------------------- /server/.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 = true 10 | 11 | [Makefile] 12 | indent_style = tab -------------------------------------------------------------------------------- /server/.eslintignore: -------------------------------------------------------------------------------- 1 | dist/**/* 2 | node_module/**/* 3 | 4 | -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | ecmaVersion: 'es2015' 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | rules: { 15 | indent: [ 16 | 'error', 17 | 2 18 | ], 19 | 'linebreak-style': [ 20 | 'error', 21 | 'unix' 22 | ], 23 | quotes: [ 24 | 'error', 25 | 'single' 26 | ], 27 | semi: [ 28 | 'error', 29 | 'never' 30 | ], 31 | "indent": [ "error", "tab", { "SwitchCase": 1 } ], 32 | // `eslint/no-unused-vars` will check all qualified ts files, include d.ts 33 | // using interface to define function types is compliant, but `eslint/no-unused-vars` will prompt for unused parameters...... 34 | // so set `args === none` here 35 | // and leave `no-unused-vars` to `@typescript-eslint/no-unused-vars` 36 | "no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": false, varsIgnorePattern: '.*' }], 37 | "@typescript-eslint/no-unused-vars": ['error'], 38 | '@typescript-eslint/no-explicit-any': 'off', 39 | '@typescript-eslint/no-var-requires': 'off', 40 | '@typescript-eslint/no-empty-interface': 'off', 41 | 'prefer-const': 'off', 42 | '@typescript-eslint/ban-ts-comment': 'off', 43 | 'prettier/prettier': 'off', 44 | '@typescript-eslint/no-this-alias': 'off' 45 | } 46 | } -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | node_modules/ 4 | dist/ 5 | tmp/ 6 | temp/ -------------------------------------------------------------------------------- /server/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'avoid', 3 | bracketSameLine: true, 4 | bracketSpacing: false, 5 | singleQuote: true, 6 | trailingComma: 'all', 7 | semi: false 8 | } 9 | -------------------------------------------------------------------------------- /server/Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | node { 4 | label '' 5 | customWorkspace 'workspace/tour-server' 6 | } 7 | } 8 | 9 | stages { 10 | stage('deploy') { 11 | steps { 12 | sh 'bash ./publish.sh' 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brightdev124/VueNode/26f08fb925904f11b088b7143b9ad5fb39feff91/server/README.md -------------------------------------------------------------------------------- /server/migration/1696406073873-update-user-default-avatar.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class UpdateUserDefaultAvatar1696406073873 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | await queryRunner.query(` 7 | UPDATE tour_user SET user_headpic = 'https://t7.baidu.com/it/u=4162611394,4275913936&fm=193&f=GIF'; 8 | `) 9 | } 10 | 11 | public async down(queryRunner: QueryRunner): Promise { 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /server/migration/1696413073247-update-tour-icon-path.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class UpdateTourIconPath1696413073247 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | await queryRunner.query(` 7 | UPDATE tour_coupon set coupon_ico_path = '//t7.baidu.com/it/u=1963305748,3425007544&fm=193' where id % 2 = 0; 8 | `) 9 | 10 | await queryRunner.query(` 11 | UPDATE tour_coupon set coupon_ico_path = '//t7.baidu.com/it/u=3596032583,931971989&fm=193' where id % 2 != 0; 12 | `) 13 | } 14 | 15 | public async down(queryRunner: QueryRunner): Promise { 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /server/migration/1696418964257-update-coupon-endtime.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class UpdateCouponEndtime1696418964257 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | await queryRunner.query(` 7 | UPDATE tour_coupon SET coupon_endtime = ${Date.now() + 3600 * 1000 * 24 * 365}; 8 | `) 9 | } 10 | 11 | public async down(queryRunner: QueryRunner): Promise { 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /server/migration/1696423773558-update-coupon_recived_num-in-coupon-table.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm' 2 | 3 | export class UpdateCouponRecivedNumInCouponTable1696423773558 implements MigrationInterface { 4 | 5 | public async up(queryRunner: QueryRunner): Promise { 6 | await queryRunner.query(` 7 | ALTER TABLE tour_coupon CHANGE coupon_recived_num coupon_received_num int NOT NULL DEFAULT 0; 8 | `) 9 | } 10 | 11 | public async down(queryRunner: QueryRunner): Promise { 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /server/nginx.conf: -------------------------------------------------------------------------------- 1 | # 仅是部分对于项目的配置,其他配置项可自定义 2 | 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | http { 8 | include /etc/nginx/conf.d/*.conf; 9 | 10 | server { 11 | listen 443 ssl http2; 12 | server_name api.0351zhuangxiu.com; 13 | 14 | gzip on; 15 | gzip_disable "msie6"; 16 | gzip_min_length 1k; 17 | gzip_vary on; 18 | gzip_types text/plain text/css application/json application/x-javascript application/javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon; 19 | 20 | location ^~ /tour/.*/.(js|css|png|jpg|jpeg) { 21 | add_header Cache-Control max-age=3600; 22 | add_header Pragma max-age=3600; 23 | } 24 | 25 | location ^~ /tour/ { 26 | proxy_pass http://127.0.0.1:8091; 27 | } 28 | 29 | ssl_certificate /etc/nginx/ssl/api.0351zhuangxiu.com/ssl.crt; 30 | ssl_certificate_key /etc/nginx/ssl/api.0351zhuangxiu.com/ssl.key; 31 | } 32 | 33 | server { 34 | listen 80; 35 | server_name api.0351zhuangxiu.com; 36 | rewrite ^(.*) https://$host$1 permanent; 37 | } 38 | } -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TypeORMDemoWithKoa", 3 | "version": "0.0.1", 4 | "description": "Awesome project developed with TypeORM.", 5 | "type": "commonjs", 6 | "scripts": { 7 | "dev": "nodemon --watch src --exec 'ts-node' src/index.ts", 8 | "build": "tsc", 9 | "typeorm": "typeorm-ts-node-commonjs", 10 | "migration:create": "typeorm migration:create", 11 | "migration:run": "npx typeorm-ts-node-esm -d ./src/data-source.ts migration:run", 12 | "lint": "eslint --ext .ts src/**", 13 | "lint:fix": "eslint --fix --ext .ts src/**" 14 | }, 15 | "dependencies": { 16 | "@koa/router": "^12.0.0", 17 | "class-validator": "^0.14.0", 18 | "koa": "^2.14.2", 19 | "koa-bodyparser": "^4.4.1", 20 | "mysql": "^2.14.1", 21 | "reflect-metadata": "^0.1.13", 22 | "typeorm": "0.3.17" 23 | }, 24 | "devDependencies": { 25 | "@types/koa": "^2.13.9", 26 | "@types/koa__router": "^12.0.1", 27 | "@types/koa-bodyparser": "^4.3.10", 28 | "@types/node": "^16.11.10", 29 | "@typescript-eslint/eslint-plugin": "^6.7.3", 30 | "@typescript-eslint/parser": "^6.7.3", 31 | "eslint": "^8.50.0", 32 | "eslint-config-prettier": "^9.0.0", 33 | "eslint-plugin-prettier": "^5.0.0", 34 | "nodemon": "^3.0.1", 35 | "prettier": "^3.0.3", 36 | "ts-node": "10.7.0", 37 | "typescript": "4.5.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server/pm2.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "tour", 5 | "script": "./dist/index.js", 6 | "watch": false, 7 | "node_args": "--harmony", 8 | "merge_logs": false, 9 | "cwd": "./", 10 | "instance": 1, 11 | "exec_mode": "cluster", 12 | "max_memory_restart": "50M", 13 | "log_date_format": "yyyy-MM-DD HH:mZ" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /server/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # publish 4 | tar -czvf tour.tar.gz . 5 | 6 | mkdir -p /work/api/tour 7 | rm -rf /work/api/tour/* 8 | cp tour.tar.gz /work/api/tour 9 | cd /work/api/tour && tar -xvf tour.tar.gz && npm --registry=https://registry.npm.taobao.org install && npm run build && pm2 stop pm2.json && pm2 start pm2.json 10 | -------------------------------------------------------------------------------- /server/src/controller/auth.controller.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Context } from 'koa' 3 | 4 | import { createHash } from 'crypto' 5 | 6 | import { GetPhoneCodeDto, LoginFormDto, RegistFormDto, ResetPasswordDto } from '../dto' 7 | 8 | import { AuthRepository, UserRepository } from '../repository' 9 | 10 | const md5 = createHash('md5') 11 | 12 | export class AuthController { 13 | public static async loginForm (ctx: Context) { 14 | const { phone, pwd } = ctx.request.body as LoginFormDto 15 | const _pwd = md5.update(pwd).digest('hex') 16 | const res = await AuthRepository.login({ 17 | phone, 18 | pwd: _pwd 19 | }) 20 | 21 | if (res) { 22 | ctx.body = { 23 | apiCode: 0, 24 | message: '登录成功', 25 | data: res.id 26 | } 27 | } else { 28 | ctx.body = { 29 | apiCode: -1, 30 | message: '登录失败,请检查手机号或密码是否正确' 31 | } 32 | } 33 | } 34 | 35 | public static async registForm (ctx: Context) { 36 | const { phone, pwd } = ctx.request.body as RegistFormDto 37 | const _pwd = md5.update(pwd).digest('hex') 38 | 39 | const user = await UserRepository.findUserByPhone({ 40 | phone 41 | }) 42 | 43 | if (user) { 44 | ctx.body = { 45 | apiCode: 0, 46 | message: '该手机号已被注册,请直接登录' 47 | } 48 | return 49 | } 50 | 51 | await AuthRepository.regist({ 52 | phone, 53 | pwd: _pwd 54 | }) 55 | 56 | ctx.body = { 57 | apiCode: 0, 58 | message: '注册成功,请用手机号密码登录' 59 | } 60 | } 61 | 62 | public static async getPhoneCode (ctx: Context) { 63 | const { phone } = ctx.request.body as GetPhoneCodeDto 64 | 65 | const user = await UserRepository.findUserByPhone({ 66 | phone 67 | }) 68 | 69 | if (!user) { 70 | ctx.body = { 71 | apiCode: 0, 72 | message: '该手机号尚未注册' 73 | } 74 | return 75 | } 76 | 77 | let code = '' 78 | for (let i = 0; i < 6; i += 1) { 79 | code += Math.floor(Math.random() * 10) 80 | } 81 | 82 | ctx.body = { 83 | apiCode: 0, 84 | message: '手机验证码已发送', 85 | data: code 86 | } 87 | } 88 | 89 | /** 90 | * 简易处理,实际应该同时传入新旧密码 91 | * @param ctx 92 | */ 93 | public static async resetPassword (ctx: Context) { 94 | const { phone, pwd, phoneCode } = ctx.request.body as ResetPasswordDto 95 | const _pwd = md5.update(pwd).digest('hex') 96 | 97 | // 需要校验 phoneCode 98 | // ...... 99 | await AuthRepository.resetPassword({ 100 | phone, 101 | pwd: _pwd, 102 | phoneCode 103 | }) 104 | 105 | ctx.body = { 106 | apiCode: 0, 107 | message: '密码重置成功' 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /server/src/controller/banner.controller.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Context } from 'koa' 3 | 4 | import { BannerRepository } from '../repository' 5 | 6 | import { FindBannerDto } from '../dto/banner.dto' 7 | 8 | export class BannerController { 9 | public static async find (ctx: Context) { 10 | const { bannerBelongRegion } = ctx.query as Partial 11 | 12 | const res = await BannerRepository.find({ 13 | bannerBelongRegion 14 | }) 15 | 16 | ctx.body = { 17 | apiCode: 0, 18 | data: res, 19 | message: '' 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server/src/controller/classify.controller.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Context } from 'koa' 3 | 4 | import { ClassifyRepository } from '../repository' 5 | 6 | export class ClassifyController { 7 | public static async find (ctx: Context) { 8 | const res = await ClassifyRepository.find() 9 | 10 | ctx.body = { 11 | apiCode: 0, 12 | data: res, 13 | message: '' 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /server/src/controller/comment.controller.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Context } from 'koa' 3 | 4 | import { CommentRepository, CouponUserRepository, UserRepository } from '../repository' 5 | 6 | import { GetCommentListDto, PublishCommentDto } from '../dto/comment.dto' 7 | 8 | export class CommentController { 9 | public static async find (ctx: Context) { 10 | const { couponId } = ctx.query as Partial 11 | 12 | const res = await CommentRepository.find({ 13 | couponId 14 | }) 15 | 16 | ctx.body = { 17 | apiCode: 0, 18 | data: res, 19 | message: '' 20 | } 21 | } 22 | 23 | public static async publish (ctx: Context) { 24 | const { userId, couponId, commentStar, commentContent } = ctx.request.body as PublishCommentDto 25 | 26 | // 按照一般的逻辑:用户购买或使用产品之后才能进行评论; 27 | // 这里的优惠券暂时没有判断什么时候就算使用了,所以测试执行以下逻辑: 28 | // 用户发表评论这个【动作】即是【使用】优惠券,优惠券使用完之后不能再次使用 29 | // 线上的项目,这块儿的逻辑可以修改下 30 | const couponUserItem = await CouponUserRepository.findById({ 31 | couponId, 32 | userId 33 | }) 34 | 35 | if (!couponUserItem) { 36 | ctx.body = { 37 | apiCode: 0, 38 | data: null, 39 | message: '购买优惠券之后才能评论哦' 40 | } 41 | return 42 | } 43 | 44 | if (couponUserItem.status && couponUserItem.status !== '0') { 45 | ctx.body = { 46 | apiCode: 0, 47 | data: null, 48 | message: '已经发布过评论了' 49 | } 50 | return 51 | } 52 | 53 | const user = await UserRepository.getUserInfo({ 54 | id: userId 55 | }) 56 | 57 | await CommentRepository.publish({ 58 | commentUserPhone: user.userPhone, 59 | commentStar, 60 | commentContent, 61 | couponId 62 | }) 63 | 64 | ctx.body = { 65 | apiCode: 0, 66 | data: null, 67 | message: '评论成功' 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /server/src/controller/coupon.controller.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Context } from 'koa' 3 | 4 | import { CouponRepository, CouponUserRepository } from '../repository' 5 | 6 | import { GetCouponDetailDto, GetCouponListDto, GetCouponRecordByUserDto, GetReceivedCouponListDto, ReceiveCouponDto } from '../dto' 7 | 8 | export class CouponController { 9 | public static async find (ctx: Context) { 10 | const { page, regionId, classifyId } = ctx.query as Partial 11 | 12 | const res = await CouponRepository.list({ 13 | page: +page, 14 | regionId: +regionId, 15 | classifyId: +classifyId 16 | }) 17 | 18 | ctx.body = { 19 | apiCode: 0, 20 | data: res, 21 | message: '' 22 | } 23 | } 24 | 25 | public static async detail (ctx: Context) { 26 | const { id } = ctx.query as Partial 27 | const res = await CouponRepository.detail({ 28 | id 29 | }) 30 | 31 | ctx.body = { 32 | apiCode: 0, 33 | data: res, 34 | message: '' 35 | } 36 | } 37 | 38 | public static async receive (ctx: Context) { 39 | const { userId, couponId } = ctx.request.body as ReceiveCouponDto 40 | 41 | const hasReceived = await CouponUserRepository.findById({ 42 | userId, 43 | couponId 44 | }) 45 | 46 | if (hasReceived) { 47 | ctx.body = { 48 | apiCode: 0, 49 | data: null, 50 | message: '已经领取过了' 51 | } 52 | return 53 | } 54 | 55 | await CouponUserRepository.add({ 56 | userId, 57 | couponId 58 | }) 59 | 60 | await CouponRepository.updateReceivedNum({ 61 | couponId 62 | }) 63 | 64 | ctx.body = { 65 | apiCode: 0, 66 | data: null, 67 | message: '领取成功' 68 | } 69 | } 70 | 71 | public static async getCouponRecordByUser (ctx: Context) { 72 | const { userId } = ctx.query as Partial 73 | 74 | const res = await CouponRepository.getCouponRecordByUser({ 75 | userId 76 | }) 77 | 78 | ctx.body = { 79 | apiCode: 0, 80 | data: res, 81 | message: '' 82 | } 83 | } 84 | 85 | public static async getReceivedCouponList (ctx: Context) { 86 | const { userId, type } = ctx.query as Partial 87 | const res = await CouponUserRepository.getReceivedCouponList({ 88 | userId, 89 | type 90 | }) 91 | 92 | ctx.body = { 93 | apiCode: 0, 94 | data: res, 95 | message: '' 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /server/src/controller/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.controller' 2 | 3 | export * from './user.controller' 4 | 5 | export * from './region.controller' 6 | 7 | export * from './classify.controller' 8 | 9 | export * from './coupon.controller' 10 | 11 | export * from './comment.controller' 12 | 13 | export * from './banner.controller' 14 | -------------------------------------------------------------------------------- /server/src/controller/region.controller.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Context } from 'koa' 3 | 4 | import { RegionRepository } from '../repository' 5 | 6 | export class RegionController { 7 | public static async find (ctx: Context) { 8 | const res = await RegionRepository.find() 9 | 10 | ctx.body = { 11 | apiCode: 0, 12 | data: res, 13 | message: '' 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /server/src/controller/user.controller.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Context } from 'koa' 3 | 4 | import { UserRepository } from '../repository' 5 | 6 | import { ChangeUserNameDto, ChangeUserSexDto } from '../dto' 7 | 8 | 9 | export class UserController { 10 | public static async getUserInfo (ctx: Context) { 11 | const { id } = ctx.query 12 | 13 | const res = await UserRepository.getUserInfo({ 14 | id: +id 15 | }) 16 | 17 | let message = res ? '' : '用户不存在' 18 | 19 | ctx.body = { 20 | apiCode: 0, 21 | data: res, 22 | message 23 | } 24 | } 25 | 26 | public static async changeUserName (ctx: Context) { 27 | const { userId, userName } = ctx.request.body as ChangeUserNameDto 28 | 29 | await UserRepository.changeUserName({ 30 | userName, 31 | userId 32 | }) 33 | 34 | ctx.body = { 35 | apiCode: 0, 36 | message: '用户名修改成功' 37 | } 38 | } 39 | 40 | public static async changeUserSex (ctx: Context) { 41 | const { userId, sex } = ctx.request.body as ChangeUserSexDto 42 | 43 | await UserRepository.changeUserSex({ 44 | sex, 45 | userId 46 | }) 47 | 48 | ctx.body = { 49 | apiCode: 0, 50 | message: '性别修改成功' 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /server/src/data-source.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from 'typeorm' 2 | 3 | export const TourDataSource = new DataSource({ 4 | type: 'mysql', 5 | host: 'localhost', 6 | port: 3306, 7 | username: 'root', 8 | password: '123456', 9 | database: 'tour', 10 | // 只能通过 migration 手动同步数据库,不允许在这个地方同步 11 | synchronize: false, 12 | logging: false, 13 | entities: [__dirname + '/entity/*.entity{.ts,.js}'], 14 | migrations: [__dirname + '../migration/*{.ts,.js}'], 15 | }) 16 | 17 | export function initORM () { 18 | return TourDataSource.initialize() 19 | .then(() => { 20 | console.log('Data Source has been initialized!') 21 | }) 22 | .catch((error) => { 23 | console.error('Error during Data Source initialization', error) 24 | }) 25 | } -------------------------------------------------------------------------------- /server/src/dto/banner.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNumberString, IsOptional } from 'class-validator' 2 | 3 | export class FindBannerDto { 4 | @IsNumberString() 5 | @IsOptional() 6 | id?: number 7 | 8 | @IsNumberString() 9 | @IsOptional() 10 | bannerBelongRegion?: number 11 | } 12 | -------------------------------------------------------------------------------- /server/src/dto/comment.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsNumber, IsNumberString, IsOptional, IsString, Length } from 'class-validator' 2 | 3 | export class GetCommentListDto { 4 | @IsNumberString() 5 | couponId: number 6 | } 7 | 8 | export class PublishCommentDto { 9 | @IsNumber() 10 | userId?: number 11 | 12 | @IsNumber() 13 | @IsNotEmpty() 14 | commentStar: number 15 | 16 | @IsString() 17 | @IsNotEmpty() 18 | @Length(10, 100) 19 | commentContent: string 20 | 21 | @IsNumber() 22 | @IsNotEmpty() 23 | couponId: number 24 | 25 | @IsString() 26 | @Length(11, 11) 27 | @IsOptional() 28 | commentUserPhone?: string 29 | 30 | @IsNumber() 31 | @Length(13, 13) 32 | @IsOptional() 33 | commentTime?: number 34 | } -------------------------------------------------------------------------------- /server/src/dto/coupon-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNumberString, IsNotEmpty } from 'class-validator' 2 | 3 | export class FindCouponUserByIdDto { 4 | @IsNumberString() 5 | @IsNotEmpty() 6 | couponId: number 7 | 8 | @IsNumberString() 9 | @IsNotEmpty() 10 | userId: number 11 | } 12 | 13 | export class AddCouponUserDto { 14 | @IsNumberString() 15 | @IsNotEmpty() 16 | couponId: number 17 | 18 | @IsNumberString() 19 | @IsNotEmpty() 20 | userId: number 21 | } 22 | -------------------------------------------------------------------------------- /server/src/dto/coupon.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsNumber, IsNumberString, IsString } from 'class-validator' 2 | 3 | export class GetCouponListDto { 4 | @IsNumberString() 5 | regionId: number 6 | 7 | @IsNumberString() 8 | classifyId: number 9 | 10 | @IsNumberString() 11 | page: number 12 | } 13 | 14 | export class GetCouponDetailDto { 15 | @IsNumberString() 16 | id: number 17 | } 18 | 19 | export class ReceiveCouponDto { 20 | @IsNumber() 21 | @IsNotEmpty() 22 | couponId: number 23 | 24 | @IsNumber() 25 | @IsNotEmpty() 26 | userId: number 27 | } 28 | 29 | export class UpdateReceivedNumDto { 30 | @IsNumber() 31 | @IsNotEmpty() 32 | couponId: number 33 | } 34 | 35 | export class GetCouponRecordByUserDto { 36 | @IsNumberString() 37 | @IsNotEmpty() 38 | userId: number 39 | } 40 | 41 | export class GetReceivedCouponListDto { 42 | @IsNumberString() 43 | @IsNotEmpty() 44 | userId: number 45 | 46 | @IsString() 47 | @IsNotEmpty() 48 | type: string 49 | } -------------------------------------------------------------------------------- /server/src/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.dto' 2 | 3 | export * from './coupon.dto' 4 | 5 | export * from './coupon-user.dto' -------------------------------------------------------------------------------- /server/src/dto/user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNumber, IsNumberString, IsString, Length, Max, MaxLength } from 'class-validator' 2 | 3 | export class LoginFormDto { 4 | @IsString() 5 | @Length(11) 6 | phone: string 7 | 8 | @IsString() 9 | pwd: string 10 | } 11 | 12 | export class FindUserByPhoneDto { 13 | @IsString() 14 | @Length(11, 11) 15 | phone: string 16 | } 17 | 18 | export class RegistFormDto { 19 | @IsString() 20 | @Length(11, 11) 21 | phone: string 22 | 23 | @IsString() 24 | @Length(6) 25 | pwd: string 26 | } 27 | 28 | export class GetPhoneCodeDto { 29 | @IsString() 30 | @Length(11, 11) 31 | phone: string 32 | } 33 | 34 | export class ResetPasswordDto { 35 | @IsString() 36 | @Length(11, 11) 37 | phone: string 38 | 39 | @IsString() 40 | @Length(6, 6) 41 | phoneCode: string 42 | 43 | @IsString() 44 | pwd: string 45 | } 46 | 47 | export class GetUserInfoDto { 48 | @IsNumberString() 49 | id: number 50 | } 51 | 52 | export class ChangeUserNameDto { 53 | @IsNumberString() 54 | userId: number 55 | 56 | @MaxLength(60) 57 | userName: string 58 | } 59 | 60 | export class ChangeUserSexDto { 61 | @IsNumberString() 62 | userId: number 63 | 64 | @IsNumber() 65 | @Max(1) 66 | sex: number 67 | } 68 | -------------------------------------------------------------------------------- /server/src/entity/banner.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm' 2 | 3 | @Entity({ 4 | name: 'tour_banner' 5 | }) 6 | export class Banner extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | id: number 9 | 10 | @Column({ 11 | name: 'banner_path', 12 | type: 'varchar', 13 | length: 255 14 | }) 15 | bannerPath: string 16 | 17 | @Column({ 18 | name: 'banner_belong_region', 19 | type: 'int', 20 | nullable: true 21 | }) 22 | bannerBelongRegion: number 23 | } 24 | -------------------------------------------------------------------------------- /server/src/entity/classify.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm' 2 | 3 | @Entity({ 4 | name: 'tour_classify' 5 | }) 6 | export class Classify extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | id: number 9 | 10 | @Column({ 11 | name: 'classify_name', 12 | type: 'varchar', 13 | length: 10 14 | }) 15 | classifyName: string 16 | } 17 | -------------------------------------------------------------------------------- /server/src/entity/comment.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm' 2 | 3 | @Entity({ 4 | name: 'tour_comment' 5 | }) 6 | export class Comment extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | id: number 9 | 10 | @Column({ 11 | name: 'comment_content', 12 | type: 'text' 13 | }) 14 | commentContent: string 15 | 16 | @Column({ 17 | name: 'comment_star', 18 | type: 'tinyint', 19 | width: 1 20 | }) 21 | commentStar: number 22 | 23 | @Column({ 24 | name: 'comment_user_phone', 25 | type: 'varchar', 26 | length: 11 27 | }) 28 | commentUserPhone: string 29 | 30 | @Column({ 31 | name: 'comment_time', 32 | type: 'int', 33 | width: 13 34 | }) 35 | commentTime: number 36 | 37 | @Column({ 38 | name: 'comment_coupon_id' 39 | }) 40 | commentCouponId: number 41 | } 42 | -------------------------------------------------------------------------------- /server/src/entity/coupon-user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm' 2 | 3 | @Entity({ 4 | name: 'tour_coupon_user' 5 | }) 6 | export class CouponUser extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | id: number 9 | 10 | @Column({ 11 | name: 'coupon_id', 12 | type: 'int' 13 | }) 14 | couponId: number 15 | 16 | @Column({ 17 | name: 'user_id', 18 | type: 'int' 19 | }) 20 | userId: number 21 | 22 | @Column({ 23 | name: 'status', 24 | type: 'enum', 25 | enumName: 'couponStatus', 26 | comment: '当前优惠券是否已被使用' 27 | }) 28 | status: '0' | '1' 29 | } 30 | -------------------------------------------------------------------------------- /server/src/entity/coupon.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm' 2 | 3 | @Entity({ 4 | name: 'tour_coupon' 5 | }) 6 | export class Coupon extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | id: number 9 | 10 | @Column({ 11 | name: 'coupon_name', 12 | type: 'varchar', 13 | length: 50 14 | }) 15 | couponName: string 16 | 17 | @Column({ 18 | name: 'coupon_explain', 19 | type: 'varchar', 20 | length: 40 21 | }) 22 | couponExplain: string 23 | 24 | @Column({ 25 | name: 'coupon_starttime', 26 | type: 'varchar', 27 | length: 15 28 | }) 29 | couponStartTime: string 30 | 31 | @Column({ 32 | name: 'coupon_endtime', 33 | type: 'varchar', 34 | length: 15 35 | }) 36 | couponEndTime: string 37 | 38 | @Column({ 39 | name: 'coupon_received_num', 40 | type: 'varchar', 41 | length: 255 42 | }) 43 | couponReceivedNum: string 44 | 45 | @Column({ 46 | name: 'coupon_detail', 47 | type: 'text' 48 | }) 49 | couponDetail: string 50 | 51 | @Column({ 52 | name: 'coupon_type', 53 | type: 'varchar', 54 | length: 20 55 | }) 56 | couponType: string 57 | 58 | @Column({ 59 | name: 'coupon_ico_path', 60 | type: 'varchar', 61 | length: 255 62 | }) 63 | couponIcoPath: string 64 | 65 | @Column({ 66 | name: 'coupon_status', 67 | type: 'varchar', 68 | length: 1, 69 | default: '0' 70 | }) 71 | couponStatus: string 72 | 73 | @Column({ 74 | name: 'coupon_classify', 75 | type: 'varchar', 76 | length: 1, 77 | default: '1' 78 | }) 79 | couponClassify: string 80 | 81 | @Column({ 82 | name: 'coupon_belong_region', 83 | type: 'varchar', 84 | length: 1, 85 | default: '1' 86 | }) 87 | couponBelongRegin: string 88 | } 89 | -------------------------------------------------------------------------------- /server/src/entity/feature.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm' 2 | 3 | @Entity({ 4 | name: 'tour_feature' 5 | }) 6 | export class Feature extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | id: number 9 | 10 | @Column({ 11 | name: 'feature_title', 12 | type: 'varchar', 13 | length: 60 14 | }) 15 | featureTitle: string 16 | 17 | @Column({ 18 | name: 'feature_content', 19 | type: 'text' 20 | }) 21 | featureContent: string 22 | 23 | @Column({ 24 | name: 'feature_ico_path', 25 | type: 'varchar', 26 | length: 255 27 | }) 28 | featureIcoPath: string 29 | 30 | @Column({ 31 | name: 'feature_url', 32 | type: 'varchar', 33 | length: 255 34 | }) 35 | featureUrl: string 36 | 37 | @Column({ 38 | name: 'feature_classify', 39 | type: 'int', 40 | default: 1 41 | }) 42 | featureClassify: number 43 | 44 | @Column({ 45 | name: 'feature_belong_region', 46 | type: 'int', 47 | default: 1 48 | }) 49 | featureBelongRegin: number 50 | } 51 | -------------------------------------------------------------------------------- /server/src/entity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './banner.entity' 2 | export * from './classify.entity' 3 | export * from './comment.entity' 4 | export * from './coupon.entity' 5 | export * from './coupon-user.entity' 6 | export * from './feature.entity' 7 | export * from './region.entity' 8 | export * from './user.entity' 9 | -------------------------------------------------------------------------------- /server/src/entity/region.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm' 2 | 3 | @Entity({ 4 | name: 'tour_region' 5 | }) 6 | export class Region extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | id: number 9 | 10 | @Column({ 11 | name: 'region_name', 12 | type: 'varchar', 13 | length: 50 14 | }) 15 | regionName: string 16 | } 17 | -------------------------------------------------------------------------------- /server/src/entity/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm' 2 | 3 | @Entity({ 4 | name: 'tour_user' 5 | }) 6 | export class User extends BaseEntity { 7 | @PrimaryGeneratedColumn() 8 | id: number 9 | 10 | @Column({ 11 | name: 'user_name', 12 | type: 'varchar', 13 | length: 60, 14 | default: '锦囊团用户' 15 | }) 16 | userName: string 17 | 18 | @Column({ 19 | name: 'user_phone', 20 | type: 'varchar', 21 | length: 11 22 | }) 23 | userPhone: string 24 | 25 | @Column({ 26 | name: 'user_pwd', 27 | type: 'varchar', 28 | length: 255 29 | }) 30 | userPwd: string 31 | 32 | @Column({ 33 | name: 'user_headpic', 34 | type: 'varchar', 35 | length: 255, 36 | default: 'http://jinnangtuan.com/static/img/users/jinnangusers/head.png' 37 | }) 38 | userHeadPic: string 39 | 40 | @Column({ 41 | name: 'user_sex', 42 | type: 'tinyint', 43 | width: 1, 44 | default: 1 45 | }) 46 | userSex: number 47 | } 48 | -------------------------------------------------------------------------------- /server/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | 3 | import Koa from 'koa' 4 | 5 | import { router } from './router' 6 | 7 | import bodyParser from 'koa-bodyparser' 8 | 9 | import { initORM } from './data-source' 10 | 11 | import { cors, error } from './middleware' 12 | 13 | initORM() 14 | 15 | const app = new Koa() 16 | 17 | app.use(error) 18 | 19 | app.use(cors) 20 | 21 | app.use(bodyParser()) 22 | 23 | app.use(router.routes()) 24 | 25 | app.use(router.allowedMethods()) 26 | 27 | app.listen(8091, '127.0.0.1', () => { 28 | console.log('App listen port 8091 successfully !') 29 | }) 30 | 31 | -------------------------------------------------------------------------------- /server/src/middleware/cors.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Context, Next } from 'koa' 2 | 3 | export async function cors (ctx: Context, next: Next) { 4 | // 正式环境不建议用 * 5 | ctx.set('access-control-allow-origin', '*') 6 | ctx.set('access-control-allow-headers', 'Origin, X-Requested-With, Content-Type, Accept, Access-Control-Expose-Headers, Platform, Token, Uid') 7 | ctx.set('access-control-allow-methods', 'PUT, POST, GET, DELETE, OPTIONS, HEAD') 8 | 9 | await next() 10 | } -------------------------------------------------------------------------------- /server/src/middleware/error.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Context, Next } from 'koa' 2 | 3 | export async function error (ctx: Context, next: Next) { 4 | try { 5 | await next() 6 | } catch (e) { 7 | console.log('error in middleware: ', e) 8 | ctx.body = { 9 | apiCode: 500, 10 | message: JSON.stringify(e), 11 | data: null 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /server/src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from './validate-request.middlelware' 2 | export * from './cors.middleware' 3 | 4 | export * from './error.middleware' -------------------------------------------------------------------------------- /server/src/middleware/validate-request.middlelware.ts: -------------------------------------------------------------------------------- 1 | import { Next, Context } from 'koa' 2 | 3 | import { validate } from 'class-validator' 4 | 5 | export function validateQuery (Dto: any) { 6 | return async function (ctx: Context, next: Next) { 7 | return _validate(ctx.query as Partial, Dto, ctx, next) 8 | } 9 | } 10 | 11 | export function validateParams (Dto: any) { 12 | return async function (ctx: Context, next: Next) { 13 | return _validate(ctx.params, Dto, ctx, next) 14 | } 15 | } 16 | 17 | export function validateBody (Dto: any) { 18 | return async function (ctx: Context, next: Next) { 19 | const body = ctx.request.body as Partial 20 | return _validate(body, Dto, ctx, next) 21 | } 22 | } 23 | 24 | async function _validate (data: NodeJS.Dict, Dto: any, ctx: Context, next: Next) { 25 | const instance = new Dto() 26 | 27 | Object.keys(data).forEach(key => { 28 | instance[key] = data[key] 29 | }) 30 | 31 | const res = await validate(instance) 32 | 33 | if (res.length) { 34 | ctx.body = { 35 | apiCode: -1, 36 | message: res.map(item => { 37 | const items: string[] = [] 38 | Object.keys(item.constraints).forEach(key => { 39 | items.push(item.constraints[key]) 40 | }) 41 | return items.join(',') 42 | }).join('. ') 43 | } 44 | return 45 | } 46 | 47 | await next() 48 | } 49 | -------------------------------------------------------------------------------- /server/src/repository/auth.repository.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../entity' 2 | 3 | import { TourDataSource } from '../data-source' 4 | 5 | import { LoginFormDto, RegistFormDto, ResetPasswordDto } from '../dto' 6 | 7 | const userRepository = TourDataSource.getRepository('User') 8 | 9 | export class AuthRepository { 10 | public static async login (options: LoginFormDto) { 11 | return await userRepository.findOneBy({ 12 | userPhone: options.phone, 13 | userPwd: options.pwd 14 | }) 15 | } 16 | 17 | public static async regist (options: RegistFormDto) { 18 | return await userRepository.insert({ 19 | userPhone: options.phone, 20 | userPwd: options.pwd 21 | }) 22 | } 23 | 24 | public static async resetPassword (options: ResetPasswordDto) { 25 | return await userRepository.update({ 26 | userPhone: options.phone 27 | }, { 28 | userPwd: options.pwd 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/src/repository/banner.repository.ts: -------------------------------------------------------------------------------- 1 | import { Banner } from '../entity' 2 | 3 | import { TourDataSource } from '../data-source' 4 | 5 | import { FindBannerDto } from '../dto/banner.dto' 6 | 7 | const bannerRepository = TourDataSource.getRepository('Banner') 8 | 9 | export class BannerRepository { 10 | public static async find (options: FindBannerDto) { 11 | return await bannerRepository.findBy(options) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/src/repository/classify.repository.ts: -------------------------------------------------------------------------------- 1 | import { Classify } from '../entity' 2 | 3 | import { TourDataSource } from '../data-source' 4 | 5 | const classifyRepository = TourDataSource.getRepository('Classify') 6 | 7 | export class ClassifyRepository { 8 | public static async find () { 9 | return await classifyRepository.find() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /server/src/repository/comment.repository.ts: -------------------------------------------------------------------------------- 1 | import { Comment } from '../entity' 2 | 3 | import { TourDataSource } from '../data-source' 4 | 5 | import { GetCommentListDto, PublishCommentDto } from '../dto/comment.dto' 6 | 7 | const commentRepository = TourDataSource.getRepository('Comment') 8 | 9 | export class CommentRepository { 10 | public static async find (options: GetCommentListDto) { 11 | return await commentRepository 12 | .createQueryBuilder('comment') 13 | .select([ 14 | 'comment.commentUserPhone as commentUserPhone', 15 | 'comment.commentStar as commentStar', 16 | 'comment.commentContent as commentContent' 17 | ]) 18 | .where('comment.commentCouponId = :couponId') 19 | .setParameters({ 20 | couponId: options.couponId 21 | }) 22 | .getRawMany() 23 | } 24 | 25 | public static async publish (options: PublishCommentDto) { 26 | return await commentRepository 27 | .insert({ 28 | commentUserPhone: options.commentUserPhone, 29 | commentStar: options.commentStar, 30 | commentContent: options.commentContent, 31 | commentTime: Date.now(), 32 | commentCouponId: options.couponId 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /server/src/repository/coupon-user.repository.ts: -------------------------------------------------------------------------------- 1 | import { Coupon, CouponUser } from '../entity' 2 | 3 | import { TourDataSource } from '../data-source' 4 | 5 | import { AddCouponUserDto, FindCouponUserByIdDto, GetReceivedCouponListDto } from '../dto' 6 | 7 | const couponUserRepository = TourDataSource.getRepository('CouponUser') 8 | 9 | export class CouponUserRepository { 10 | public static async findById (options: FindCouponUserByIdDto) { 11 | const { userId, couponId } = options 12 | return await couponUserRepository 13 | .createQueryBuilder('couponUser') 14 | .select([ 15 | 'couponUser.couponId', 16 | 'couponUser.userId', 17 | 'couponUser.status' 18 | ]) 19 | .where('couponUser.couponId = :couponId') 20 | .andWhere('couponUser.userId = :userId') 21 | .setParameters({ 22 | couponId, 23 | userId, 24 | }) 25 | .getOne() 26 | } 27 | 28 | public static async add (options: AddCouponUserDto) { 29 | const { userId, couponId } = options 30 | return await couponUserRepository 31 | .insert({ 32 | userId, 33 | couponId 34 | }) 35 | } 36 | 37 | public static async getReceivedCouponList (options: GetReceivedCouponListDto) { 38 | const { userId, type } = options 39 | 40 | const qb = couponUserRepository.createQueryBuilder('couponUser') 41 | 42 | const list = await qb 43 | .select('couponUser.couponId') 44 | .where('couponUser.userId = :userId', { userId }) 45 | .getMany() 46 | 47 | const ids = list.map(item => item.couponId) 48 | 49 | // 其实可以不用查 couponType,在前端分类也行 50 | return await qb 51 | .leftJoin(Coupon, 'coupon', 'coupon.id = couponUser.couponId') 52 | .select([ 53 | 'coupon.id as id', 54 | 'coupon.couponName as couponName', 55 | 'coupon.couponExplain as couponExplain', 56 | 'coupon.couponIcoPath as couponIconPath', 57 | 'coupon.couponReceivedNum as couponReceivedNum', 58 | 'couponUser.status' 59 | ]) 60 | .where(`coupon.couponStatus = "0" 61 | and coupon.couponType = :type 62 | and coupon.id In (:...ids) 63 | and couponUser.userId = :userId`, 64 | { 65 | ids, 66 | userId, 67 | type 68 | } 69 | ) 70 | .getRawMany() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /server/src/repository/coupon.repository.ts: -------------------------------------------------------------------------------- 1 | import { Comment, Coupon, CouponUser } from '../entity' 2 | 3 | import { TourDataSource } from '../data-source' 4 | 5 | import { GetCouponDetailDto, GetCouponListDto, GetCouponRecordByUserDto, UpdateReceivedNumDto } from '../dto' 6 | 7 | const couponRepository = TourDataSource.getRepository('Coupon') 8 | 9 | export class CouponRepository { 10 | public static async list (options: GetCouponListDto) { 11 | const baseQuery = couponRepository 12 | .createQueryBuilder('coupon') 13 | .select([ 14 | 'coupon.id', 15 | 'coupon.couponName', 16 | 'coupon.couponExplain', 17 | 'coupon.couponIcoPath', 18 | 'coupon.couponStatus', 19 | 'coupon.couponReceivedNum' 20 | ]) 21 | .take(5) 22 | .skip((options.page - 1) * 5) 23 | .orderBy('coupon.id', 'DESC') 24 | 25 | if (+options.regionId === 1) { 26 | return await baseQuery 27 | .where('coupon.couponBelongRegin = :regionId', { regionId: +options.regionId }) 28 | .getMany() 29 | } 30 | 31 | return await baseQuery 32 | .andWhere('coupon.couponClassify = :classifyId', { classifyId: +options.classifyId }) 33 | .getMany() 34 | } 35 | 36 | public static async detail (options: GetCouponDetailDto) { 37 | return await couponRepository 38 | .createQueryBuilder('coupon') 39 | .leftJoin(Comment, 'comment', 'coupon.id = comment.commentCouponId') 40 | .select([ 41 | 'coupon.couponName as couponName', 42 | 'coupon.couponExplain as couponExplain', 43 | 'coupon.couponStartTime as couponStartTime', 44 | 'coupon.couponEndTime as couponEndTime', 45 | 'coupon.couponIcoPath as couponIcoPath', 46 | 'comment.commentContent as commentContent', 47 | 'comment.commentStar as commentStar', 48 | 'comment.commentUserPhone as commentUserPhone' 49 | ]) 50 | .where('coupon.id = :id') 51 | .setParameters({ 52 | id: options.id 53 | }) 54 | .getRawOne() 55 | } 56 | 57 | public static async updateReceivedNum (options: UpdateReceivedNumDto) { 58 | const coupon = await couponRepository.findOneBy({ 59 | id: options.couponId 60 | }) 61 | if (coupon) { 62 | coupon.couponReceivedNum += 1 63 | } 64 | return couponRepository.save(coupon) 65 | } 66 | 67 | public static async getCouponRecordByUser (options: GetCouponRecordByUserDto) { 68 | const { userId } = options 69 | 70 | // return await couponRepository 71 | // .createQueryBuilder('coupon') 72 | // .select([ 73 | // 'COUNT(coupon.couponName) as num', 74 | // 'coupon.couponType' 75 | // ]) 76 | // .groupBy('coupon.couponType') 77 | // .where("id in (select coupon_id from tour_coupon_user where user_id=:userId)", { userId }) 78 | // .getRawMany() 79 | 80 | const qb = couponRepository.createQueryBuilder('coupon') 81 | 82 | return await qb 83 | .select([ 84 | 'COUNT(coupon.couponName) as num', 85 | 'coupon.couponType as couponType' 86 | ]) 87 | .where('id In ' + 88 | qb 89 | .subQuery() 90 | .select('couponUser.couponId') 91 | .from(CouponUser, 'couponUser') 92 | .where('couponUser.userId = :userId', { userId }) 93 | .getQuery() 94 | ) 95 | .groupBy('coupon.couponType') 96 | .getRawMany() 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /server/src/repository/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.repository' 2 | 3 | export * from './user.repository' 4 | 5 | export * from './region.repository' 6 | 7 | export * from './classify.repository' 8 | 9 | export * from './coupon.repository' 10 | 11 | export * from './comment.repository' 12 | 13 | export * from './coupon-user.repository' 14 | 15 | export * from './banner.repository' -------------------------------------------------------------------------------- /server/src/repository/region.repository.ts: -------------------------------------------------------------------------------- 1 | import { Region } from '../entity' 2 | 3 | import { TourDataSource } from '../data-source' 4 | 5 | const regionRepository = TourDataSource.getRepository('Region') 6 | 7 | export class RegionRepository { 8 | public static async find () { 9 | return await regionRepository.find() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /server/src/repository/user.repository.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../entity' 2 | 3 | import { TourDataSource } from '../data-source' 4 | 5 | import { ChangeUserNameDto, ChangeUserSexDto, FindUserByPhoneDto, GetUserInfoDto } from '../dto' 6 | 7 | const userRepository = TourDataSource.getRepository('User') 8 | 9 | export class UserRepository { 10 | public static async getUserInfo (options: GetUserInfoDto) { 11 | return await userRepository 12 | .createQueryBuilder('user') 13 | .select(['user.userName', 'user.userPhone', 'user.userHeadPic', 'user.userSex']) 14 | .where('user.id = :id', { id: options.id }) 15 | .getOne() 16 | } 17 | 18 | public static async changeUserName (options: ChangeUserNameDto) { 19 | return await userRepository.update({ 20 | id: options.userId 21 | }, { 22 | userName: options.userName 23 | }) 24 | } 25 | 26 | public static async changeUserSex (options: ChangeUserSexDto) { 27 | return await userRepository.update({ 28 | id: options.userId 29 | }, { 30 | userSex: options.sex 31 | }) 32 | } 33 | 34 | public static async findUserByPhone (options: FindUserByPhoneDto) { 35 | return await userRepository.findOneBy({ 36 | userPhone: options.phone 37 | }) 38 | } 39 | } -------------------------------------------------------------------------------- /server/src/router/auth.router.ts: -------------------------------------------------------------------------------- 1 | import Router from '@koa/router' 2 | 3 | import { AuthController } from '../controller' 4 | 5 | import { validateBody } from '../middleware' 6 | 7 | import { GetPhoneCodeDto, LoginFormDto, RegistFormDto, ResetPasswordDto } from '../dto' 8 | 9 | export const authRouter = new Router() 10 | 11 | authRouter.post( 12 | 'loginForm', 13 | '/tour/auth/loginForm', 14 | validateBody(LoginFormDto), 15 | AuthController.loginForm 16 | ) 17 | 18 | authRouter.post( 19 | 'registForm', 20 | '/tour/auth/registForm', 21 | validateBody(RegistFormDto), 22 | AuthController.registForm 23 | ) 24 | 25 | authRouter.post( 26 | 'getPhoneCode', 27 | '/tour/auth/getPhoneCode', 28 | validateBody(GetPhoneCodeDto), 29 | AuthController.getPhoneCode 30 | ) 31 | 32 | authRouter.post( 33 | 'resetPassword', 34 | '/tour/auth/resetPassword', 35 | validateBody(ResetPasswordDto), 36 | AuthController.resetPassword 37 | ) 38 | 39 | 40 | -------------------------------------------------------------------------------- /server/src/router/banner.router.ts: -------------------------------------------------------------------------------- 1 | import Router from '@koa/router' 2 | 3 | import { BannerController } from '../controller' 4 | import { validateQuery } from '../middleware' 5 | import { FindBannerDto } from '../dto/banner.dto' 6 | 7 | export const bannerRouter = new Router() 8 | 9 | bannerRouter.get( 10 | 'getRegionList', 11 | '/tour/banner/get', 12 | validateQuery(FindBannerDto), 13 | BannerController.find 14 | ) 15 | 16 | 17 | -------------------------------------------------------------------------------- /server/src/router/classify.router.ts: -------------------------------------------------------------------------------- 1 | import Router from '@koa/router' 2 | 3 | import { ClassifyController } from '../controller' 4 | 5 | export const classifyRouter = new Router() 6 | 7 | classifyRouter.get( 8 | 'getClassifyList', 9 | '/tour/classify/list', 10 | ClassifyController.find 11 | ) 12 | 13 | 14 | -------------------------------------------------------------------------------- /server/src/router/comment.router.ts: -------------------------------------------------------------------------------- 1 | import Router from '@koa/router' 2 | 3 | import { CommentController } from '../controller' 4 | 5 | import { validateBody, validateQuery } from '../middleware' 6 | 7 | import { GetCommentListDto, PublishCommentDto } from '../dto/comment.dto' 8 | 9 | export const commentRouter = new Router() 10 | 11 | commentRouter.get( 12 | 'getCommentList', 13 | '/tour/comment/get', 14 | validateQuery(GetCommentListDto), 15 | CommentController.find 16 | ) 17 | 18 | commentRouter.post( 19 | 'publishComment', 20 | '/tour/comment/publish', 21 | validateBody(PublishCommentDto), 22 | CommentController.publish 23 | ) 24 | 25 | 26 | -------------------------------------------------------------------------------- /server/src/router/coupon.router.ts: -------------------------------------------------------------------------------- 1 | import Router from '@koa/router' 2 | 3 | import { CouponController } from '../controller' 4 | 5 | import { validateBody, validateQuery } from '../middleware' 6 | 7 | import { GetCouponDetailDto, GetCouponListDto, ReceiveCouponDto, GetCouponRecordByUserDto, GetReceivedCouponListDto } from '../dto' 8 | 9 | export const couponRouter = new Router() 10 | 11 | couponRouter.get( 12 | 'getCouponList', 13 | '/tour/coupon/list', 14 | validateQuery(GetCouponListDto), 15 | CouponController.find 16 | ) 17 | 18 | couponRouter.get( 19 | 'getCouponDetail', 20 | '/tour/coupon/detail', 21 | validateQuery(GetCouponDetailDto), 22 | CouponController.detail 23 | ) 24 | 25 | couponRouter.post( 26 | 'receiveCoupon', 27 | '/tour/coupon/receive', 28 | validateBody(ReceiveCouponDto), 29 | CouponController.receive 30 | ) 31 | 32 | couponRouter.get( 33 | 'getCouponRecordByUser', 34 | '/tour/coupon/record', 35 | validateQuery(GetCouponRecordByUserDto), 36 | CouponController.getCouponRecordByUser 37 | ) 38 | 39 | couponRouter.get( 40 | 'getReceivedCouponList', 41 | '/tour/coupon/received', 42 | validateQuery(GetReceivedCouponListDto), 43 | CouponController.getReceivedCouponList 44 | ) 45 | 46 | 47 | -------------------------------------------------------------------------------- /server/src/router/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import Router from '@koa/router' 3 | 4 | import { authRouter } from './auth.router' 5 | 6 | import { userRouter } from './user.router' 7 | 8 | import { regionRouter } from './region.router' 9 | 10 | import { classifyRouter } from './classify.router' 11 | 12 | import { couponRouter } from './coupon.router' 13 | 14 | import { commentRouter } from './comment.router' 15 | 16 | import { bannerRouter } from './banner.router' 17 | 18 | export const router = new Router() 19 | 20 | router.use(authRouter.routes()) 21 | 22 | router.use(userRouter.routes()) 23 | 24 | router.use(regionRouter.routes()) 25 | 26 | router.use(classifyRouter.routes()) 27 | 28 | router.use(couponRouter.routes()) 29 | 30 | router.use(commentRouter.routes()) 31 | 32 | router.use(bannerRouter.routes()) 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /server/src/router/region.router.ts: -------------------------------------------------------------------------------- 1 | import Router from '@koa/router' 2 | 3 | import { RegionController } from '../controller' 4 | 5 | export const regionRouter = new Router() 6 | 7 | regionRouter.get( 8 | 'getRegionList', 9 | '/tour/region/list', 10 | RegionController.find 11 | ) 12 | 13 | 14 | -------------------------------------------------------------------------------- /server/src/router/user.router.ts: -------------------------------------------------------------------------------- 1 | import Router from '@koa/router' 2 | 3 | import { validateBody, validateQuery } from '../middleware' 4 | 5 | import { UserController } from '../controller/user.controller' 6 | 7 | import { ChangeUserNameDto, ChangeUserSexDto, GetUserInfoDto } from '../dto' 8 | 9 | export const userRouter = new Router() 10 | 11 | userRouter.get( 12 | 'getUserInfo', 13 | '/tour/user/info', 14 | validateQuery(GetUserInfoDto), 15 | UserController.getUserInfo 16 | ) 17 | 18 | userRouter.post( 19 | 'changeUserName', 20 | '/tour/user/changeUserName', 21 | validateBody(ChangeUserNameDto), 22 | UserController.changeUserName 23 | ) 24 | 25 | userRouter.post( 26 | 'changeUserSex', 27 | '/tour/user/changeUserSex', 28 | validateBody(ChangeUserSexDto), 29 | UserController.changeUserSex 30 | ) 31 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "allowSyntheticDefaultImports": true, 6 | "experimentalDecorators": true, 7 | "emitDecoratorMetadata": true, 8 | "skipLibCheck": true, 9 | "target": "es6", 10 | "noImplicitAny": true, 11 | "moduleResolution": "node", 12 | "sourceMap": true, 13 | "outDir": "dist", 14 | "baseUrl": ".", 15 | "rootDir": "." 16 | }, 17 | "include": ["src/**/*"], 18 | "exclude": ["node_modules"] 19 | } 20 | --------------------------------------------------------------------------------