├── .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 | 
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 | 
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
2 |
3 |
4 |
5 | {{ columnName }}
6 |
7 |
8 |
9 |
10 |
21 |
22 |
52 |
--------------------------------------------------------------------------------
/fe/src/components/count-down/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
18 |
19 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
![]()
10 |
11 |
{{ coupon.couponName }}
12 |
{{ coupon.couponExplain }}
13 |
14 | 活动时间:{{ dateFormate(coupon.couponStartTime) }}至{{
15 | dateFormate(coupon.couponEndTime)
16 | }}
17 |
18 |
19 |
20 |
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 |
2 |
3 |
35 |
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 |
2 |
3 |
4 |
5 |
10 |
【优惠券有效期】
11 |
12 | 自领取日起5天内有效
13 |
14 |
【活动范围】
15 |
所有银联卡(卡号以62开头)
16 |
【活动地点】
17 |
莎莎香港及澳门所有门店
18 |
【活动内容】
19 |
20 |
21 | 1、优惠活动期内,银联卡(卡号以62开头)持卡人在莎莎香港及澳门门店消费单笔购物金额满港币或澳门币100元或以上,经验证以二维码或条形码为形式的莎莎电子现金券后使用银联卡支付,立减港币或澳门币30元;
22 |
23 |
2、具体电子现金券可使用时间以券显示有效期为准;
24 |
3、每一次交易仅可使用一张电子现金券,不可拆分,不能提现;
25 |
4、每人每天在同一店内只可使用最多3张电子现金券;
26 |
5、必须于付款前声明使用且主动出示电子现金券;
27 |
6、交易必须通过银联网络付款,方可享有此优惠;
28 |
7、本活动仅限莎莎香港及澳门所有门店;
29 |
8、活动详情以店内信息为准;
30 |
9、本活动的最终解释权归莎莎、锦囊团和银联国际共同所有。
31 |
32 |
33 |
34 |
42 |
47 | 查看详情
48 |
49 |
50 |
51 |
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 |
2 |
3 |
52 |
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 |
2 |
3 |
9 |
10 |
11 |
39 |
40 |
69 |
--------------------------------------------------------------------------------
/fe/src/components/star/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ v.star }}
15 |
16 |
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 |
2 |
3 |
9 |
10 |
13 |
14 |
22 |
23 |
29 |
30 |
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 |
2 |
3 |
4 |
5 |
6 |

12 |
13 |
45 |
46 |
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 |
2 |
3 |
4 |
5 |
6 |
13 | 登录
14 |
15 |
22 | 注册
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
55 |
56 |
85 |
--------------------------------------------------------------------------------
/fe/src/pages/account/login.vue:
--------------------------------------------------------------------------------
1 |
2 |
37 |
38 |
39 |
96 |
97 |
100 |
--------------------------------------------------------------------------------
/fe/src/pages/account/regist.vue:
--------------------------------------------------------------------------------
1 |
2 |
43 |
44 |
45 |
112 |
113 |
116 |
--------------------------------------------------------------------------------
/fe/src/pages/account/reset-password.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |

13 |
14 |
43 |
44 |
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 |
2 |
3 |
4 |
5 |
6 |
26 |
27 |
28 |
43 |
44 |
45 |
63 |
64 |
65 |
66 |
76 |
77 |
![]()
81 |
82 |
83 |
{{ v.couponName }}
84 |
85 | {{ v.couponExplain }}
86 |
87 |
88 |
89 |
已抢
90 |
{{ v.couponReceivedNum }}
91 |
98 |
99 |
100 |
101 |
102 |
加载更多
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
277 |
278 |
364 |
--------------------------------------------------------------------------------
/fe/src/pages/personal/change-headpic.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
17 |
18 |
19 |
20 |
21 |
59 |
60 |
92 |
--------------------------------------------------------------------------------
/fe/src/pages/personal/change-user-name.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
17 |
18 |
19 |
20 |
21 |
82 |
83 |
97 |
--------------------------------------------------------------------------------
/fe/src/pages/personal/change-user-sex.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
17 |
18 |
19 |
20 |
28 |
29 |
30 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
35 |
36 | 用户名称
37 |
38 |
39 | {{ state.userName }}
40 |
41 |
42 |
50 |
51 | 性别
52 |
53 |
54 | 男
55 | 女
56 |
57 |
58 |
59 |
60 |
61 |
62 |
65 |
66 |
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 |
2 | {{ state.userInfo }}
3 |
4 |
5 |
31 |
--------------------------------------------------------------------------------
/fe/src/pages/wechat/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
--------------------------------------------------------------------------------