├── weapp ├── src │ ├── app.scss │ ├── constants │ │ └── counter.js │ ├── pages │ │ └── index │ │ │ ├── index.scss │ │ │ └── index.jsx │ ├── reducers │ │ ├── index.js │ │ └── counter.js │ ├── actions │ │ └── counter.js │ ├── store │ │ └── index.js │ ├── index.html │ └── app.jsx ├── .temp │ ├── app.scss │ ├── constants │ │ └── counter.js │ ├── pages │ │ └── index │ │ │ ├── index.scss │ │ │ └── index.js │ ├── reducers │ │ ├── index.js │ │ └── counter.js │ ├── actions │ │ └── counter.js │ ├── store │ │ └── index.js │ ├── index.html │ └── app.js ├── config │ ├── dev.js │ ├── prod.js │ └── index.js ├── .eslintrc ├── .editorconfig ├── project.config.json └── package.json ├── images ├── home.png ├── category.png └── signup.png ├── h5 ├── public │ ├── robots.txt │ ├── favicon.ico │ └── index.html ├── .babelrc ├── jsconfig.json ├── src │ ├── assets │ │ ├── 404 │ │ │ └── 404.jpg │ │ ├── home │ │ │ ├── mi.png │ │ │ ├── search.png │ │ │ ├── arrow-up.png │ │ │ ├── banner01.jpg │ │ │ ├── banner02.jpg │ │ │ ├── banner03.jpg │ │ │ ├── cate │ │ │ │ ├── 智能.png │ │ │ │ ├── 米粉卡.png │ │ │ │ ├── 以旧换新.png │ │ │ │ ├── 小米众筹.png │ │ │ │ ├── 小米秒杀.png │ │ │ │ ├── 洗衣机热卖.png │ │ │ │ ├── 电视热卖.png │ │ │ │ ├── 笔记本热卖.png │ │ │ │ ├── 超值特卖.png │ │ │ │ └── K30-Pro.png │ │ │ └── arrow-down.png │ │ ├── login │ │ │ └── mi.png │ │ ├── star │ │ │ ├── 发布.png │ │ │ ├── user.png │ │ │ └── message-bell.png │ │ ├── cart │ │ │ └── cart.png │ │ ├── user │ │ │ ├── avatar.png │ │ │ └── background.png │ │ ├── common │ │ │ └── 下载app.png │ │ ├── search │ │ │ ├── delete.png │ │ │ └── arrow-left.png │ │ ├── category │ │ │ ├── K30 Pro.png │ │ │ ├── 小米10 青春.png │ │ │ ├── banner01.jpg │ │ │ └── K30 Pro 变焦版.png │ │ └── tabbar │ │ │ ├── icon-cart.png │ │ │ ├── icon-home.png │ │ │ ├── icon-star.png │ │ │ ├── icon-user.png │ │ │ ├── icon-category.png │ │ │ ├── icon-cart-selected.png │ │ │ ├── icon-home-selected.png │ │ │ ├── icon-star-selected.png │ │ │ ├── icon-user-selected.png │ │ │ └── icon-category-selected.png │ ├── pages │ │ ├── List │ │ │ └── List.jsx │ │ ├── Home │ │ │ ├── Content │ │ │ │ ├── TV │ │ │ │ │ └── TV.jsx │ │ │ │ ├── Laptop │ │ │ │ │ └── Laptop.jsx │ │ │ │ ├── Others │ │ │ │ │ └── Others.jsx │ │ │ │ ├── Appliance │ │ │ │ │ └── Appliance.jsx │ │ │ │ ├── Cellphone │ │ │ │ │ └── Cellphone.jsx │ │ │ │ ├── SmartDevice │ │ │ │ │ └── SmartDevice.jsx │ │ │ │ ├── Content.jsx │ │ │ │ ├── content.config.js │ │ │ │ └── Recommend │ │ │ │ │ └── Recommend.jsx │ │ │ ├── Home.jsx │ │ │ ├── Header │ │ │ │ ├── header.module.scss │ │ │ │ └── Header.jsx │ │ │ └── Navbar │ │ │ │ ├── navbar.module.scss │ │ │ │ └── Navbar.jsx │ │ ├── Star │ │ │ ├── Content │ │ │ │ ├── 5G.jsx │ │ │ │ ├── MiLife.jsx │ │ │ │ ├── Recommend.jsx │ │ │ │ └── content.config.js │ │ │ ├── Star.jsx │ │ │ └── Header.jsx │ │ ├── LandingPage │ │ │ └── LandingPage.jsx │ │ ├── Search │ │ │ ├── Search.jsx │ │ │ ├── Header │ │ │ │ ├── header.module.scss │ │ │ │ └── Header.jsx │ │ │ └── History.jsx │ │ ├── NotFound │ │ │ ├── notFound.module.scss │ │ │ └── NotFound.jsx │ │ ├── User │ │ │ ├── User.jsx │ │ │ └── user.module.scss │ │ ├── Category │ │ │ ├── LeftPanel │ │ │ │ ├── leftPanel.module.scss │ │ │ │ └── LeftPanel.jsx │ │ │ ├── Category.jsx │ │ │ └── RightPanel │ │ │ │ ├── rightPanel.module.scss │ │ │ │ └── RightPanel.jsx │ │ ├── Cart │ │ │ ├── cart.module.scss │ │ │ └── Cart.jsx │ │ └── Login │ │ │ ├── login.module.scss │ │ │ └── Login.jsx │ ├── components │ │ ├── Layout │ │ │ └── AppLayout.jsx │ │ ├── Header │ │ │ ├── header.module.scss │ │ │ └── Header.jsx │ │ ├── Cate │ │ │ └── Cate.jsx │ │ ├── Tabbar │ │ │ ├── tabbar.module.scss │ │ │ └── Tabbar.jsx │ │ ├── Banner │ │ │ ├── Pagination.jsx │ │ │ ├── PaginationDot.jsx │ │ │ └── Banner.jsx │ │ └── DownloadApp │ │ │ └── DownloadApp.jsx │ ├── index.jsx │ ├── store │ │ ├── reducers │ │ │ ├── reducerCreator.js │ │ │ ├── categoryReducer.js │ │ │ ├── cartReducer.js │ │ │ ├── authReducer.js │ │ │ ├── commonReducer.js │ │ │ ├── index.js │ │ │ ├── homeReducer.js │ │ │ └── searchReducer.js │ │ ├── index.js │ │ ├── actionTypes │ │ │ └── types.js │ │ └── actionCreators │ │ │ └── index.js │ ├── styles │ │ ├── global.scss │ │ └── reset.scss │ ├── App.jsx │ ├── router │ │ ├── PrivateRoute.jsx │ │ ├── Loading.jsx │ │ ├── router.config.js │ │ └── AppRouter.jsx │ └── utils │ │ └── request │ │ ├── config.js │ │ └── index.js ├── .gitignore ├── .eslintrc ├── package.json └── README.md ├── server ├── .prettierrc.js ├── src │ ├── api │ │ ├── index.ts │ │ └── routes │ │ │ └── auth.ts │ ├── interfaces │ │ └── IUser.ts │ ├── loaders │ │ ├── database.ts │ │ ├── index.ts │ │ ├── logger.ts │ │ └── server.ts │ ├── models │ │ └── user.model.ts │ ├── app.ts │ ├── config │ │ └── index.ts │ └── services │ │ └── auth.service.ts ├── .vscode │ └── launch.json ├── .eslintrc.js ├── package.json └── tsconfig.json ├── doc ├── 后端接口文档.md └── 前端组件树.md ├── package.json ├── README.md ├── LICENSE └── .gitignore /weapp/src/app.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /weapp/.temp/app.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /weapp/.temp/constants/counter.js: -------------------------------------------------------------------------------- 1 | export const ADD = 'ADD'; 2 | export const MINUS = 'MINUS'; -------------------------------------------------------------------------------- /weapp/src/constants/counter.js: -------------------------------------------------------------------------------- 1 | export const ADD = 'ADD' 2 | export const MINUS = 'MINUS' 3 | -------------------------------------------------------------------------------- /images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/images/home.png -------------------------------------------------------------------------------- /h5/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /images/category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/images/category.png -------------------------------------------------------------------------------- /images/signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/images/signup.png -------------------------------------------------------------------------------- /weapp/.temp/pages/index/index.scss: -------------------------------------------------------------------------------- 1 | .index { 2 | flex-direction: column; 3 | width: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /weapp/src/pages/index/index.scss: -------------------------------------------------------------------------------- 1 | .index { 2 | flex-direction: column; 3 | width: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /h5/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/public/favicon.ico -------------------------------------------------------------------------------- /h5/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react", "@babel/preset-env"], 3 | "plugins": ["emotion"] 4 | } 5 | -------------------------------------------------------------------------------- /h5/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src" 4 | }, 5 | "include": ["src"] 6 | } 7 | -------------------------------------------------------------------------------- /h5/src/assets/404/404.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/404/404.jpg -------------------------------------------------------------------------------- /h5/src/assets/home/mi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/home/mi.png -------------------------------------------------------------------------------- /h5/src/assets/login/mi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/login/mi.png -------------------------------------------------------------------------------- /h5/src/assets/star/发布.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/star/发布.png -------------------------------------------------------------------------------- /h5/src/assets/cart/cart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/cart/cart.png -------------------------------------------------------------------------------- /h5/src/assets/home/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/home/search.png -------------------------------------------------------------------------------- /h5/src/assets/star/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/star/user.png -------------------------------------------------------------------------------- /h5/src/assets/user/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/user/avatar.png -------------------------------------------------------------------------------- /h5/src/assets/common/下载app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/common/下载app.png -------------------------------------------------------------------------------- /h5/src/assets/home/arrow-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/home/arrow-up.png -------------------------------------------------------------------------------- /h5/src/assets/home/banner01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/home/banner01.jpg -------------------------------------------------------------------------------- /h5/src/assets/home/banner02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/home/banner02.jpg -------------------------------------------------------------------------------- /h5/src/assets/home/banner03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/home/banner03.jpg -------------------------------------------------------------------------------- /h5/src/assets/home/cate/智能.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/home/cate/智能.png -------------------------------------------------------------------------------- /h5/src/assets/home/cate/米粉卡.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/home/cate/米粉卡.png -------------------------------------------------------------------------------- /h5/src/assets/search/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/search/delete.png -------------------------------------------------------------------------------- /h5/src/assets/category/K30 Pro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/category/K30 Pro.png -------------------------------------------------------------------------------- /h5/src/assets/category/小米10 青春.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/category/小米10 青春.png -------------------------------------------------------------------------------- /h5/src/assets/home/arrow-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/home/arrow-down.png -------------------------------------------------------------------------------- /h5/src/assets/home/cate/以旧换新.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/home/cate/以旧换新.png -------------------------------------------------------------------------------- /h5/src/assets/home/cate/小米众筹.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/home/cate/小米众筹.png -------------------------------------------------------------------------------- /h5/src/assets/home/cate/小米秒杀.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/home/cate/小米秒杀.png -------------------------------------------------------------------------------- /h5/src/assets/home/cate/洗衣机热卖.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/home/cate/洗衣机热卖.png -------------------------------------------------------------------------------- /h5/src/assets/home/cate/电视热卖.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/home/cate/电视热卖.png -------------------------------------------------------------------------------- /h5/src/assets/home/cate/笔记本热卖.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/home/cate/笔记本热卖.png -------------------------------------------------------------------------------- /h5/src/assets/home/cate/超值特卖.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/home/cate/超值特卖.png -------------------------------------------------------------------------------- /h5/src/assets/tabbar/icon-cart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/tabbar/icon-cart.png -------------------------------------------------------------------------------- /h5/src/assets/tabbar/icon-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/tabbar/icon-home.png -------------------------------------------------------------------------------- /h5/src/assets/tabbar/icon-star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/tabbar/icon-star.png -------------------------------------------------------------------------------- /h5/src/assets/tabbar/icon-user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/tabbar/icon-user.png -------------------------------------------------------------------------------- /h5/src/assets/user/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/user/background.png -------------------------------------------------------------------------------- /h5/src/assets/category/banner01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/category/banner01.jpg -------------------------------------------------------------------------------- /h5/src/assets/home/cate/K30-Pro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/home/cate/K30-Pro.png -------------------------------------------------------------------------------- /h5/src/assets/search/arrow-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/search/arrow-left.png -------------------------------------------------------------------------------- /h5/src/assets/star/message-bell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/star/message-bell.png -------------------------------------------------------------------------------- /h5/src/pages/List/List.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function List() { 4 | return
列表页
; 5 | } 6 | -------------------------------------------------------------------------------- /h5/src/assets/category/K30 Pro 变焦版.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/category/K30 Pro 变焦版.png -------------------------------------------------------------------------------- /h5/src/assets/tabbar/icon-category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/tabbar/icon-category.png -------------------------------------------------------------------------------- /h5/src/pages/Home/Content/TV/TV.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function TV() { 4 | return
电视
; 5 | } 6 | -------------------------------------------------------------------------------- /h5/src/pages/Star/Content/5G.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Three() { 4 | return
5G
; 5 | } 6 | -------------------------------------------------------------------------------- /h5/src/pages/Star/Content/MiLife.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Two() { 4 | return
MiLife
; 5 | } 6 | -------------------------------------------------------------------------------- /h5/src/assets/tabbar/icon-cart-selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/tabbar/icon-cart-selected.png -------------------------------------------------------------------------------- /h5/src/assets/tabbar/icon-home-selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/tabbar/icon-home-selected.png -------------------------------------------------------------------------------- /h5/src/assets/tabbar/icon-star-selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/tabbar/icon-star-selected.png -------------------------------------------------------------------------------- /h5/src/assets/tabbar/icon-user-selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/tabbar/icon-user-selected.png -------------------------------------------------------------------------------- /h5/src/pages/Home/Content/Laptop/Laptop.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Laptop() { 4 | return
笔记本
; 5 | } 6 | -------------------------------------------------------------------------------- /h5/src/pages/Star/Content/Recommend.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function One() { 4 | return
Recommend
; 5 | } 6 | -------------------------------------------------------------------------------- /h5/src/pages/Home/Content/Others/Others.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Others() { 4 | return
生活周边
; 5 | } 6 | -------------------------------------------------------------------------------- /h5/src/assets/tabbar/icon-category-selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnscorpions/xiaomi-shop-clone/HEAD/h5/src/assets/tabbar/icon-category-selected.png -------------------------------------------------------------------------------- /h5/src/pages/Home/Content/Appliance/Appliance.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Appliance() { 4 | return
家用电器
; 5 | } 6 | -------------------------------------------------------------------------------- /h5/src/pages/Home/Content/Cellphone/Cellphone.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Cellphone() { 4 | return
手机
; 5 | } 6 | -------------------------------------------------------------------------------- /h5/src/pages/LandingPage/LandingPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function LandingPage() { 4 | return
着陆页,下载小米商城App
; 5 | } 6 | -------------------------------------------------------------------------------- /h5/src/pages/Home/Content/SmartDevice/SmartDevice.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function SmartDevice() { 4 | return
智能设备
; 5 | } 6 | -------------------------------------------------------------------------------- /weapp/.temp/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import counter from './counter'; 3 | 4 | export default combineReducers({ 5 | counter 6 | }); -------------------------------------------------------------------------------- /weapp/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import counter from './counter' 3 | 4 | export default combineReducers({ 5 | counter 6 | }) 7 | -------------------------------------------------------------------------------- /server/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 4, 7 | }; 8 | -------------------------------------------------------------------------------- /weapp/config/dev.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | NODE_ENV: '"development"' 4 | }, 5 | defineConstants: { 6 | }, 7 | mini: {}, 8 | h5: {} 9 | } 10 | -------------------------------------------------------------------------------- /server/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import auth from './routes/auth'; 3 | 4 | export default () => { 5 | const app = Router(); 6 | auth(app); 7 | 8 | return app; 9 | }; 10 | -------------------------------------------------------------------------------- /weapp/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["taro"], 3 | "rules": { 4 | "no-unused-vars": ["error", { "varsIgnorePattern": "Taro" }], 5 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx", ".tsx"] }] 6 | }, 7 | "parser": "babel-eslint" 8 | } 9 | -------------------------------------------------------------------------------- /h5/src/components/Layout/AppLayout.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Tabbar from "../Tabbar/Tabbar"; 3 | 4 | export default function AppLayout({ content }) { 5 | return ( 6 | <> 7 | {content} 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /weapp/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /h5/src/pages/Search/Search.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Header from "./Header/Header"; 3 | import History from "./History"; 4 | 5 | export default function Search() { 6 | return ( 7 |
8 |
9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /server/src/interfaces/IUser.ts: -------------------------------------------------------------------------------- 1 | export interface IUser { 2 | _id: string; 3 | name: string; 4 | email: string; // 支持email验证,登录 5 | password: string; 6 | salt: string; // 随机盐值,用于用户鉴权 7 | } 8 | 9 | export interface UserDTO { 10 | email: string; 11 | password: string; 12 | name?: string; 13 | } 14 | -------------------------------------------------------------------------------- /weapp/project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "miniprogramRoot": "./dist", 3 | "projectname": "weapp", 4 | "description": "仿小米商城微信小程序前端", 5 | "appid": "touristappid", 6 | "setting": { 7 | "urlCheck": true, 8 | "es6": false, 9 | "postcss": false, 10 | "minified": false 11 | }, 12 | "compileType": "miniprogram" 13 | } 14 | -------------------------------------------------------------------------------- /doc/后端接口文档.md: -------------------------------------------------------------------------------- 1 | 架构:Express + Typescript + Mongodb + redis + Docker 2 | 3 | # 架构 4 | 5 | - 决定后端架构 6 | 7 | 1. [Bulletproof node.js project architecture][1] 8 | 2. [Nest 官方文档][2] 9 | 10 | # 功能 11 | 12 | - 通知推送 13 | - 阅读统计 14 | - 数据埋点 15 | - [x] 登录/注册/第三方登录 16 | - [x] 注册 17 | - [x] 登录 18 | 19 | [1]: https://github.com/santiq/bulletproof-nodejs 20 | [2]: https://docs.nestjs.com/ 21 | -------------------------------------------------------------------------------- /h5/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | // 区分host 6 | if (process.env.NODE_ENV === "development") { 7 | process.env.HOST = process.env.REACT_APP_DEV_HOST; 8 | } else { 9 | process.env.HOST = process.env.REACT_APP_PROD_HOST; 10 | } 11 | 12 | ReactDOM.render(, document.getElementById("root")); 13 | -------------------------------------------------------------------------------- /h5/src/pages/Home/Content/Content.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | import { ContentConfig } from "./content.config"; 4 | 5 | export default function Content() { 6 | const selector = useSelector((state) => state.home.nav); 7 | const Content = ContentConfig[selector]; 8 | 9 | return ( 10 |
11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /h5/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /weapp/.temp/actions/counter.js: -------------------------------------------------------------------------------- 1 | import { ADD, MINUS } from '../constants/counter'; 2 | 3 | export const add = () => { 4 | return { 5 | type: ADD 6 | }; 7 | }; 8 | export const minus = () => { 9 | return { 10 | type: MINUS 11 | }; 12 | }; 13 | 14 | // 异步的action 15 | export function asyncAdd() { 16 | return dispatch => { 17 | setTimeout(() => { 18 | dispatch(add()); 19 | }, 2000); 20 | }; 21 | } -------------------------------------------------------------------------------- /server/src/loaders/database.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { Db } from 'mongodb'; 3 | import config from '../config'; 4 | 5 | export default async (): Promise => { 6 | const connection = await mongoose.connect(config.databaseURL, { 7 | useNewUrlParser: true, 8 | useUnifiedTopology: true, 9 | useCreateIndex: true, 10 | }); 11 | return connection.connection.db; 12 | }; 13 | -------------------------------------------------------------------------------- /h5/src/store/reducers/reducerCreator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * reducer helper function 3 | * @param {*} initialState 4 | * @param {*} handlers 5 | */ 6 | export const createReducer = (initialState, handlers) => { 7 | return function reducer(state = initialState, action) { 8 | if (handlers.hasOwnProperty(action.type)) { 9 | return handlers[action.type](state, action); 10 | } else { 11 | return state; 12 | } 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /weapp/src/actions/counter.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD, 3 | MINUS 4 | } from '../constants/counter' 5 | 6 | export const add = () => { 7 | return { 8 | type: ADD 9 | } 10 | } 11 | export const minus = () => { 12 | return { 13 | type: MINUS 14 | } 15 | } 16 | 17 | // 异步的action 18 | export function asyncAdd () { 19 | return dispatch => { 20 | setTimeout(() => { 21 | dispatch(add()) 22 | }, 2000) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /h5/src/pages/Home/Home.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import DownloadApp from "components/DownloadApp/DownloadApp"; 4 | import Header from "./Header/Header"; 5 | import Navbar from "./Navbar/Navbar"; 6 | import Content from "./Content/Content"; 7 | 8 | export default function Home() { 9 | return ( 10 | <> 11 | 12 |
13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /weapp/config/prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | NODE_ENV: '"production"' 4 | }, 5 | defineConstants: { 6 | }, 7 | mini: {}, 8 | h5: { 9 | /** 10 | * 如果h5端编译后体积过大,可以使用webpack-bundle-analyzer插件对打包体积进行分析。 11 | * 参考代码如下: 12 | * webpackChain (chain) { 13 | * chain.plugin('analyzer') 14 | * .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, []) 15 | * } 16 | */ 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /h5/src/store/reducers/categoryReducer.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from "./reducerCreator"; 2 | import * as types from "../actionTypes/types"; 3 | 4 | const initialState = { 5 | setCate: "01" 6 | }; 7 | 8 | const CategoryReducer = createReducer(initialState, { 9 | [types.SET_CATE]: (state = initialState, action) => { 10 | return { 11 | ...state, 12 | setCate: action.payload 13 | }; 14 | } 15 | }); 16 | 17 | export default CategoryReducer; 18 | -------------------------------------------------------------------------------- /h5/src/components/Header/header.module.scss: -------------------------------------------------------------------------------- 1 | .search-container { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100vw; 6 | display: flex; 7 | flex-wrap: nowrap; 8 | justify-content: space-between; 9 | align-items: center; 10 | height: 5rem; 11 | background: #f2f2f2; 12 | color: #666; 13 | img { 14 | margin: 0 1rem; 15 | width: 2rem; 16 | } 17 | .text { 18 | color: rgb(102, 102, 102); 19 | font-size: 1.6rem; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /h5/src/store/reducers/cartReducer.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from "./reducerCreator"; 2 | import * as types from "../actionTypes/types"; 3 | 4 | const initialState = { 5 | goods: [] 6 | }; 7 | 8 | const CartReducer = createReducer(initialState, { 9 | [types.ADD_GOODS_TO_CART]: (state = initialState, action) => { 10 | return { 11 | ...state, 12 | goods: [...state.goods, action.payload] 13 | }; 14 | } 15 | }); 16 | 17 | export default CartReducer; 18 | -------------------------------------------------------------------------------- /h5/src/store/reducers/authReducer.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from "./reducerCreator"; 2 | import * as types from "../actionTypes/types"; 3 | 4 | const initialState = { 5 | isAuthed: false, 6 | token: null 7 | }; 8 | 9 | const AuthReducer = createReducer(initialState, { 10 | [types.LOGIN]: (state = initialState, action) => { 11 | return { 12 | isAuthed: true, 13 | token: action.payload 14 | }; 15 | } 16 | }); 17 | 18 | export default AuthReducer; 19 | -------------------------------------------------------------------------------- /h5/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "react-app", 4 | "airbnb", 5 | "plugin:jsx-a11y/recommended", 6 | "prettier", 7 | "prettier/react" 8 | ], 9 | "plugins": ["jsx-a11y", "prettier"], 10 | "rules": { 11 | "semi": 0, 12 | "import/no-unresolved": [1], 13 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 14 | "prettier/prettier": [ 15 | "error", 16 | { 17 | "semi": false 18 | } 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /h5/src/store/reducers/commonReducer.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from "./reducerCreator"; 2 | import * as types from "../actionTypes/types"; 3 | 4 | const initialState = { 5 | isShowDownloadAppBanner: true 6 | }; 7 | 8 | const CommonReducer = createReducer(initialState, { 9 | [types.HIDE_DOWNLOAD_APP_BANNER]: (state = initialState, action) => { 10 | return { 11 | ...state, 12 | isShowDownloadAppBanner: false 13 | }; 14 | } 15 | }); 16 | 17 | export default CommonReducer; 18 | -------------------------------------------------------------------------------- /weapp/.temp/reducers/counter.js: -------------------------------------------------------------------------------- 1 | import { ADD, MINUS } from '../constants/counter'; 2 | 3 | const INITIAL_STATE = { 4 | num: 0 5 | }; 6 | 7 | export default function counter(state = INITIAL_STATE, action) { 8 | switch (action.type) { 9 | case ADD: 10 | return { 11 | ...state, 12 | num: state.num + 1 13 | }; 14 | case MINUS: 15 | return { 16 | ...state, 17 | num: state.num - 1 18 | }; 19 | default: 20 | return state; 21 | } 22 | } -------------------------------------------------------------------------------- /h5/src/styles/global.scss: -------------------------------------------------------------------------------- 1 | @import "./reset.scss"; 2 | 3 | html, 4 | body { 5 | height: 100vh; 6 | width: 100vw; 7 | font-size: 10px; 8 | } 9 | 10 | a { 11 | text-decoration: none; 12 | } 13 | 14 | input[type="text"]::-webkit-inner-spin-button, 15 | input[type="password"]::-webkit-outer-spin-button { 16 | -webkit-appearance: none; 17 | margin: 0; 18 | } 19 | 20 | /** 21 | * 水平垂直居中 22 | */ 23 | @mixin centerIt { 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | } 28 | -------------------------------------------------------------------------------- /weapp/src/reducers/counter.js: -------------------------------------------------------------------------------- 1 | import { ADD, MINUS } from '../constants/counter' 2 | 3 | const INITIAL_STATE = { 4 | num: 0 5 | } 6 | 7 | export default function counter (state = INITIAL_STATE, action) { 8 | switch (action.type) { 9 | case ADD: 10 | return { 11 | ...state, 12 | num: state.num + 1 13 | } 14 | case MINUS: 15 | return { 16 | ...state, 17 | num: state.num - 1 18 | } 19 | default: 20 | return state 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /h5/src/pages/Star/Star.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Header from "./Header"; 3 | 4 | import publicIcon from "assets/star/发布.png"; 5 | 6 | const styles = { 7 | icon: { 8 | position: "fixed", 9 | bottom: "10rem", 10 | left: "50%", 11 | marginLeft: "-3.6rem", 12 | width: "7.2rem", 13 | }, 14 | }; 15 | 16 | export default function Star() { 17 | return ( 18 |
19 |
20 | icon-star 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /h5/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Provider } from "react-redux" 3 | import { PersistGate } from "redux-persist/integration/react" 4 | import { store, persistor } from "store/index" 5 | 6 | import AppRouter from "router/AppRouter" 7 | import "./styles/global.scss" 8 | 9 | function App() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | ) 17 | } 18 | 19 | export default App 20 | -------------------------------------------------------------------------------- /h5/src/components/Cate/Cate.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const styles = { 4 | wrapper: { 5 | display: "flex", 6 | flexWrap: "wrap", 7 | }, 8 | item: { 9 | flex: "1", 10 | width: "20%", 11 | }, 12 | }; 13 | 14 | export default function Cate(props) { 15 | const { items } = props; 16 | 17 | return ( 18 |
19 | {items.map((item, index) => ( 20 | icon 21 | ))} 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /server/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Express后端调试", 11 | "program": "${workspaceFolder}\\src\\app.ts", 12 | "outFiles": ["${workspaceFolder}/*/*.js"] 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /h5/src/pages/NotFound/notFound.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | width: 100vw; 6 | height: 100vh; 7 | text-align: center; 8 | .icon { 9 | display: block; 10 | width: 24rem; 11 | margin: 0 auto 0.5rem; 12 | } 13 | .sorry { 14 | margin: 2.4rem 0; 15 | font-size: 2.4rem; 16 | line-height: 3.6rem; 17 | } 18 | .goto { 19 | margin: 1.6rem 0; 20 | text-decoration: none; 21 | color: #0b72a4; 22 | font-size: 1.6rem; 23 | line-height: 3.6rem; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/src/loaders/index.ts: -------------------------------------------------------------------------------- 1 | import expressLoader from './server'; 2 | import mongooseLoader from './database'; 3 | import logger from './logger'; 4 | 5 | export default async (params: any) => { 6 | const { expressApp } = params; 7 | try { 8 | // 连接数据库 9 | const mongoConnection = await mongooseLoader(); 10 | logger.info('✌️ DB loaded and connected!'); 11 | 12 | // 起服务器 13 | await expressLoader({ app: expressApp }); 14 | logger.info('✌️ Express loaded'); 15 | } catch (error) { 16 | logger.error(error); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /h5/src/components/Tabbar/tabbar.module.scss: -------------------------------------------------------------------------------- 1 | .tabbar-container { 2 | display: flex; 3 | position: fixed; 4 | bottom: 0; 5 | width: 100%; 6 | height: 5.2rem; 7 | box-shadow: 0 -0.2rem 1rem 0.1rem rgba(0, 0, 0, 0.2); 8 | background-color: #fff; 9 | } 10 | 11 | .icon-box { 12 | flex: 1; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | .icon { 18 | margin: 0 auto 0.5rem; 19 | width: 2.1rem; 20 | height: 2.1rem; 21 | } 22 | .text { 23 | color: #999; 24 | } 25 | .selected { 26 | color: #ff6700; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /h5/src/store/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import CommonReducer from "./commonReducer"; 3 | import HomeReducer from "./homeReducer"; 4 | import SearchReducer from "./searchReducer"; 5 | import CategoryReducer from "./categoryReducer"; 6 | import AuthReducer from "./authReducer"; 7 | import CartReducer from "./cartReducer"; 8 | 9 | const rootReducer = combineReducers({ 10 | common: CommonReducer, 11 | home: HomeReducer, 12 | search: SearchReducer, 13 | category: CategoryReducer, 14 | auth: AuthReducer, 15 | cart: CartReducer 16 | }); 17 | 18 | export default rootReducer; 19 | -------------------------------------------------------------------------------- /h5/src/store/reducers/homeReducer.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from "./reducerCreator"; 2 | import * as types from "../actionTypes/types"; 3 | 4 | const initialState = { 5 | nav: "recommend", 6 | selectedTab: "/" 7 | }; 8 | 9 | const HomeReducer = createReducer(initialState, { 10 | [types.SET_NAV]: (state = initialState, action) => { 11 | return { 12 | ...state, 13 | nav: action.payload 14 | }; 15 | }, 16 | [types.SELECT_TAB]: (state, action) => { 17 | return { 18 | ...state, 19 | selectedTab: action.payload 20 | }; 21 | } 22 | }); 23 | 24 | export default HomeReducer; 25 | -------------------------------------------------------------------------------- /h5/src/pages/Star/Content/content.config.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Loadable from "react-loadable"; 3 | 4 | const Loading = () =>
加载中...
; 5 | 6 | const Recommend = Loadable({ 7 | loader: () => import("pages/Star/Content/Recommend"), 8 | loading: Loading 9 | }); 10 | 11 | const MiLife = Loadable({ 12 | loader: () => import("pages/Star/Content/MiLife"), 13 | loading: Loading 14 | }); 15 | 16 | const Network = Loadable({ 17 | loader: () => import("pages/Star/Content/5G"), 18 | loading: Loading 19 | }); 20 | 21 | export const ContentConfig = { 22 | Recommend, 23 | MiLife, 24 | Network 25 | }; 26 | -------------------------------------------------------------------------------- /h5/src/router/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Redirect } from "react-router-dom"; 3 | 4 | function PrivateRoute({ children, ...rest }) { 5 | let { isAuth } = { ...rest }; 6 | 7 | // 未认证跳转 8 | return ( 9 | 12 | isAuth ? ( 13 | children 14 | ) : ( 15 | 21 | ) 22 | } 23 | /> 24 | ); 25 | } 26 | 27 | export default PrivateRoute; 28 | -------------------------------------------------------------------------------- /h5/src/router/Loading.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import FadeLoader from "react-spinners/FadeLoader"; 3 | 4 | const containerStyle = { 5 | display: "flex", 6 | justifyContent: "center", 7 | alignSelf: "center" 8 | }; 9 | 10 | export default function Loading(props) { 11 | if (props.error) { 12 | return ( 13 |
14 | 错误! 15 |
16 | ); 17 | } else if (props.pastDelay) { 18 | return ( 19 |
20 | 21 |
22 | ); 23 | } else { 24 | return null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /h5/src/store/reducers/searchReducer.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from "./reducerCreator"; 2 | import * as types from "../actionTypes/types"; 3 | 4 | const initialState = { 5 | historyList: [] 6 | }; 7 | 8 | const SearchReducer = createReducer(initialState, { 9 | [types.ADD_SEARCH_HISTORY]: (state = initialState, action) => { 10 | return { 11 | ...state, 12 | historyList: [...state.historyList, action.payload] 13 | }; 14 | }, 15 | [types.REMOVE_ALL_SEARCH_HISTORY]: (state = initialState, action) => { 16 | return { 17 | ...state, 18 | historyList: [] 19 | }; 20 | } 21 | }); 22 | 23 | export default SearchReducer; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mi-shop", 3 | "version": "1.0.0", 4 | "description": "仿小米商城全栈项目,包括h5, 微信小程序,Android, SSR, Node后端", 5 | "main": "index.js", 6 | "directories": { 7 | "doc": "doc" 8 | }, 9 | "scripts": { 10 | "git": "git add . && git commit -m", 11 | "postgit": "git pull && git push && git status" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/cnscorpions/mi-shop.git" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/cnscorpions/mi-shop/issues" 21 | }, 22 | "homepage": "https://github.com/cnscorpions/mi-shop#readme" 23 | } 24 | -------------------------------------------------------------------------------- /h5/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import { persistStore, persistReducer } from "redux-persist"; 3 | import storageSession from "redux-persist/lib/storage/session"; 4 | import rootReducer from "./reducers/index"; 5 | 6 | const persistConfig = { 7 | key: "root", 8 | storage: storageSession, 9 | whitelist: ["auth", "common", "home", "search", "category"], 10 | debug: true 11 | }; 12 | 13 | const persistedReducer = persistReducer(persistConfig, rootReducer); 14 | 15 | export let store = createStore( 16 | persistedReducer, 17 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() 18 | ); 19 | 20 | export let persistor = persistStore(store); 21 | -------------------------------------------------------------------------------- /server/src/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { IUser } from '../interfaces/IUser'; 3 | 4 | const userSchema = new mongoose.Schema( 5 | { 6 | name: String, 7 | email: { 8 | type: String, 9 | lowercase: true, 10 | unique: true, 11 | index: true, 12 | }, 13 | password: String, 14 | salt: String, 15 | role: { 16 | type: String, 17 | default: 'user', 18 | }, 19 | }, 20 | { 21 | timestamps: true, 22 | }, 23 | ); 24 | 25 | const user = mongoose.model('User', userSchema); 26 | 27 | export default user; 28 | -------------------------------------------------------------------------------- /h5/src/pages/NotFound/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | import notFoundIcon from "assets/404/404.jpg"; 4 | 5 | import * as styles from "./notFound.module.scss"; 6 | 7 | export default function NotFound() { 8 | const history = useHistory(); 9 | 10 | const goBack = () => { 11 | history.push("/"); 12 | }; 13 | 14 | return ( 15 |
16 | 404 17 |

抱歉,页面找不到了...

18 |

19 | 商城首页 mi.com 20 |

21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /h5/src/store/actionTypes/types.js: -------------------------------------------------------------------------------- 1 | // 选择底部tab 2 | export const SELECT_TAB = "SELECT_TAB"; 3 | 4 | // 首页-选择nav 5 | export const SET_NAV = "SET_NAV"; 6 | 7 | // 应用-是否显示CTA 8 | export const HIDE_DOWNLOAD_APP_BANNER = "HIDE_DOWNLOAD_APP_BANNER"; 9 | 10 | // 搜索-添加/删除搜索历史 11 | export const ADD_SEARCH_HISTORY = "ADD_SEARCH_HISTORY"; 12 | export const REMOVE_ALL_SEARCH_HISTORY = "REMOVE_ALL_SEARCH_HISTORY"; 13 | 14 | // 分类-选择类目 15 | export const SET_CATE = "SET_CATE"; 16 | 17 | // Auth - login 18 | export const LOGIN = "LOGIN"; 19 | export const SIGNUP = "SIGNUP"; 20 | export const OAUTH = "OAUTH"; 21 | 22 | // 购物车 - 加入购物车 23 | export const ADD_GOODS_TO_CART = "ADD_GOODS_TO_CART"; 24 | export const REMOVE_GOODS_TO_CART = "REMOVE_GOODS_TO_CART"; 25 | -------------------------------------------------------------------------------- /h5/src/utils/request/config.js: -------------------------------------------------------------------------------- 1 | // Content-Type 2 | export const CONTENT_TYPE = { 3 | JSON: "application/json;charset=UTF-8", 4 | FORM: "application/x-www-form-urlencoded; charset=UTF-8" 5 | }; 6 | 7 | // 请求方法 8 | export const REQUEST_METHOD = { 9 | GET: "GET", 10 | POST: "POST", 11 | PUT: "PUT", 12 | PATCH: "PATCH", 13 | DELETE: "DELETE" 14 | }; 15 | 16 | // HTTP状态码 17 | export const HTTP_STATUS = { 18 | SUCCESS: 200, 19 | CREATED: 201, 20 | ACCEPTED: 202, 21 | CLIENT_ERROR: 400, 22 | AUTHENTICATE: 401, 23 | FORBIDDEN: 403, 24 | NOT_FOUND: 404, 25 | SERVER_ERROR: 500, 26 | BAD_GATEWAY: 502, 27 | SERVICE_UNAVAILABLE: 503, 28 | GATEWAY_TIMEOUT: 504 29 | }; 30 | 31 | // base url 32 | export const BASE_URL = "/api"; 33 | -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 3 | parserOptions: { 4 | ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features 5 | sourceType: 'module', // Allows for the use of imports 6 | }, 7 | extends: [ 8 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 9 | 'prettier/@typescript-eslint', 10 | 'plugin:prettier/recommended', 11 | ], 12 | rules: { 13 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 14 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /h5/src/pages/User/User.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | 4 | import userIcon from "assets/user/avatar.png"; 5 | 6 | import * as styles from "./user.module.scss"; 7 | 8 | export default function User() { 9 | const history = useHistory(); 10 | 11 | const goLogin = () => { 12 | history.push("/login"); 13 | }; 14 | 15 | return ( 16 |
17 |
18 | 用户图标 19 | 登录/注册 20 |
21 |
22 | 我的订单 23 | 全部订单 24 |
25 |
    26 |
    27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /h5/src/pages/User/user.module.scss: -------------------------------------------------------------------------------- 1 | @import "styles/global.scss"; 2 | 3 | .wrapper { 4 | .login-box { 5 | display: flex; 6 | justify-content: flex-start; 7 | align-items: center; 8 | padding: 2rem 0; 9 | background: url(../../assets/user/background.png) center 0 #f37d0f; 10 | background-size: auto 100%; 11 | & > img { 12 | margin: 0 0.8rem 0 1.6rem; 13 | width: 4.4rem; 14 | } 15 | & > span { 16 | font-size: 1.2rem; 17 | color: #fff; 18 | } 19 | } 20 | .order-box { 21 | @include centerIt; 22 | justify-content: space-between; 23 | padding: 0 1.6rem; 24 | height: 4.1rem; 25 | border-bottom: 1px solid rgba(0, 0, 0, 0.15); 26 | color: rgba(0, 0, 0, 0.87); 27 | font-size: 1.5rem; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /h5/src/pages/Category/LeftPanel/leftPanel.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | position: fixed; 3 | top: 5rem; 4 | bottom: 5.2rem; 5 | left: 0; 6 | border-right: 1px solid #efefef; 7 | width: 7.8rem; 8 | background: aqua; 9 | overflow: hidden; 10 | } 11 | 12 | .list-container { 13 | position: absolute; 14 | top: 0; 15 | bottom: 0; 16 | left: 0; 17 | right: -1.5rem; 18 | padding: 1rem 1.5rem 1rem 0; 19 | background: #fefefe; 20 | overflow-y: auto; 21 | z-index: 90; 22 | .list-item { 23 | height: 4.6rem; 24 | line-height: 4.6rem; 25 | &-text { 26 | display: block; 27 | height: 100%; 28 | font-size: 1.3rem; 29 | text-align: center; 30 | overflow: hidden; 31 | } 32 | &_active { 33 | color: #fb7d34; 34 | transform: scale(1.2); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /weapp/.temp/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunkMiddleware from 'redux-thunk'; 3 | import rootReducer from "../reducers/index"; 4 | 5 | const composeEnhancers = typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ 6 | // Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize... 7 | }) : compose; 8 | 9 | const middlewares = [thunkMiddleware]; 10 | 11 | if (process.env.NODE_ENV === 'development' && true) { 12 | middlewares.push(require('redux-logger').createLogger()); 13 | } 14 | 15 | const enhancer = composeEnhancers(applyMiddleware(...middlewares) 16 | // other store enhancers if any 17 | ); 18 | 19 | export default function configStore() { 20 | const store = createStore(rootReducer, enhancer); 21 | return store; 22 | } -------------------------------------------------------------------------------- /h5/src/pages/Home/Header/header.module.scss: -------------------------------------------------------------------------------- 1 | .search-container { 2 | display: flex; 3 | flex-wrap: nowrap; 4 | align-items: center; 5 | height: 4.4rem; 6 | background: #f2f2f2; 7 | color: #666; 8 | &-icon { 9 | display: flex; 10 | justify-content: center; 11 | width: 5.2rem; 12 | margin: 0 1rem; 13 | } 14 | &-icon > img { 15 | width: 2.5rem; 16 | } 17 | &-input { 18 | display: flex; 19 | justify-content: flex-start; 20 | align-items: center; 21 | border: 1px solid #e5e5e5; 22 | text-align: left; 23 | padding: 0.8rem 0; 24 | width: 100%; 25 | color: rgba(0, 0, 0, 0.3); 26 | background-color: #fff; 27 | border-radius: 0.04rem; 28 | font-size: 1.6rem; 29 | } 30 | &-input > img { 31 | margin: 0 0.4rem; 32 | width: 1.6rem; 33 | } 34 | &-user > img { 35 | margin: 0 1rem; 36 | width: 2.4rem; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /h5/src/components/Banner/Pagination.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PaginationDot from "./PaginationDot"; 3 | 4 | const styles = { 5 | root: { 6 | position: "absolute", 7 | bottom: "1rem", 8 | right: "50%", 9 | marginRight: "-3rem", 10 | display: "flex", 11 | flexDirection: "row" 12 | } 13 | }; 14 | 15 | const Pagination = props => { 16 | const handleClick = index => { 17 | const { onChangeIndex } = props; 18 | onChangeIndex(index); 19 | }; 20 | 21 | const { index, dots } = props; 22 | 23 | const children = []; 24 | 25 | for (let i = 0; i < dots; i += 1) { 26 | children.push( 27 | handleClick(i)} 32 | /> 33 | ); 34 | } 35 | 36 | return
    {children}
    ; 37 | }; 38 | 39 | export default Pagination; 40 | -------------------------------------------------------------------------------- /h5/src/pages/Search/Header/header.module.scss: -------------------------------------------------------------------------------- 1 | .search-container { 2 | display: flex; 3 | flex-wrap: nowrap; 4 | align-items: center; 5 | height: 4.1rem; 6 | background: #f2f2f2; 7 | color: #666; 8 | &-icon { 9 | display: flex; 10 | justify-content: center; 11 | width: 5.2rem; 12 | margin: 0 1rem; 13 | } 14 | &-icon > img { 15 | width: 2.4rem; 16 | } 17 | &-input { 18 | display: flex; 19 | justify-content: flex-start; 20 | align-items: center; 21 | border: 1px solid #e5e5e5; 22 | text-align: left; 23 | padding: 0.6rem 0 0.6rem 0.4rem; 24 | width: 100%; 25 | color: rgba(0, 0, 0, 0.3); 26 | background-color: #fff; 27 | border-radius: 0.04rem; 28 | font-size: 1.6rem; 29 | } 30 | &-input > img { 31 | margin: 0 0.4rem; 32 | width: 1.6rem; 33 | } 34 | &-user > img { 35 | margin: 0 1rem; 36 | width: 2.4rem; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /server/src/app.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | import config from './config/index'; 3 | import logger from './loaders/logger'; 4 | 5 | // Add a polyfill for the Reflect API 6 | import 'reflect-metadata'; 7 | 8 | const startServer = async () => { 9 | const app: express.Application = express(); 10 | 11 | try { 12 | await require('./loaders').default({ expressApp: app }); 13 | } catch (error) { 14 | logger.error(error); 15 | } 16 | 17 | app.listen(config.port, (err: any) => { 18 | if (err) { 19 | logger.error(err); 20 | process.exit(1); 21 | return; 22 | } 23 | logger.info(` 24 | ################################################ 25 | 🛡️ Server listening on port: ${config.port} 🛡️ 26 | ################################################ 27 | `); 28 | }); 29 | }; 30 | 31 | startServer(); 32 | -------------------------------------------------------------------------------- /h5/src/pages/Category/Category.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | 4 | import Header from "components/Header/Header"; 5 | import LeftPanel from "./LeftPanel/LeftPanel"; 6 | import RightPanel from "./RightPanel/RightPanel"; 7 | 8 | export default function Category() { 9 | const history = useHistory(); 10 | 11 | const goBack = () => { 12 | history.goBack(); 13 | }; 14 | 15 | const goSearch = () => { 16 | history.push("/search"); 17 | }; 18 | 19 | // const handleScroll = anchor => { 20 | // // 获取元素 21 | // const titleEl = document.querySelector(`span[title=${anchor}]`); 22 | // // 滚动到特定位置 23 | // }; 24 | 25 | return ( 26 | <> 27 |
    28 |
    29 | 30 | 31 |
    32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /doc/前端组件树.md: -------------------------------------------------------------------------------- 1 | 1. 页面组件 2 | 3 | - [x] 首页 4 | - [x] 头部 5 | - [x] banner 图 6 | - [x] 类目 7 | - [x] 404 8 | - [x] 搜索页 9 | - [x] 头部 10 | - [x] 搜索历史 11 | - [x] 分类 12 | - [x] 顶部栏 13 | - [x] 侧边栏 14 | - [x] 内容 15 | - [ ] 星球 16 | - [x] 顶部栏 17 | - [ ] 通知角标 18 | - [ ] 购物车 19 | - [x] 其他 20 | - [ ] 购物车操作 21 | - [ ] 我 22 | - [x] 其他 23 | - [ ] 订单操作 24 | - [ ] 服务中心 25 | - [ ] 小米中心 26 | - [ ] 登录页 27 | - [x] 基础 28 | - [x] 接口接入 29 | - [x] 表单验证 30 | - [ ] Oauth 31 | - 列表 32 | - 详情 33 | 34 | 2. 公众组件 35 | 36 | - [x] tabbar 37 | - [x] loading 38 | - [x] swiper 组件 39 | - [x] 顶部"前往 App 下载"banner 40 | - [x] 类目 41 | - [ ] 对话框 42 | - [ ] 加载进度条 43 | - [ ] 通知 44 | - [ ] 图片懒加载 45 | - 回到顶部 46 | - 回到上一页 47 | - 商品列表 48 | 49 | 3. 其他 50 | 51 | - nav 切换动画 52 | - 红包晃动动画 53 | 54 | 4. 微信小程序 55 | 56 | 5. React Native 安卓 57 | -------------------------------------------------------------------------------- /weapp/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | import thunkMiddleware from 'redux-thunk' 3 | import rootReducer from '../reducers' 4 | 5 | const composeEnhancers = 6 | typeof window === 'object' && 7 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? 8 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ 9 | // Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize... 10 | }) : compose 11 | 12 | const middlewares = [ 13 | thunkMiddleware 14 | ] 15 | 16 | if (process.env.NODE_ENV === 'development' && process.env.TARO_ENV !== 'quickapp') { 17 | middlewares.push(require('redux-logger').createLogger()) 18 | } 19 | 20 | const enhancer = composeEnhancers( 21 | applyMiddleware(...middlewares), 22 | // other store enhancers if any 23 | ) 24 | 25 | export default function configStore () { 26 | const store = createStore(rootReducer, enhancer) 27 | return store 28 | } 29 | -------------------------------------------------------------------------------- /h5/src/components/Header/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as styles from "./header.module.scss"; 3 | 4 | import arrowLeftIcon from "assets/search/arrow-left.png"; 5 | import searchIconPath from "assets/home/search.png"; 6 | 7 | export default function Header(props) { 8 | const { title, goBack, goSearch } = props; 9 | 10 | const toParentGoback = () => { 11 | goBack(); 12 | }; 13 | 14 | const toParentGoSearch = () => { 15 | goSearch(); 16 | }; 17 | 18 | return ( 19 |
    20 |
    21 | 返回上一页 22 |
    23 | {title} 24 |
    28 | 搜索 29 |
    30 |
    31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /server/src/loaders/logger.ts: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | import config from '../config'; 3 | 4 | const transports = []; 5 | 6 | /** 7 | * 设置dev/prod下的日志transport 8 | */ 9 | if (process.env.NODE_ENV !== 'development') { 10 | transports.push(new winston.transports.Console()); 11 | } else { 12 | transports.push( 13 | new winston.transports.Console({ 14 | format: winston.format.combine(winston.format.cli(), winston.format.splat()), 15 | }), 16 | ); 17 | } 18 | 19 | // 创建logger实例,并导出 20 | const logger = winston.createLogger({ 21 | level: config.logs.level, 22 | levels: winston.config.npm.levels, 23 | format: winston.format.combine( 24 | winston.format.timestamp({ 25 | format: 'YYYY-MM-DD HH:mm:ss', 26 | }), 27 | winston.format.errors({ stack: true }), 28 | winston.format.splat(), 29 | winston.format.json(), 30 | ), 31 | transports, 32 | }); 33 | 34 | export default logger; 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mi-shop 2 | 仿小米商城全栈项目,包括h5, 微信小程序,Android, SSR, Node后端 3 | 4 | ## 预览 5 | 6 | 9 | 10 | | 首页 | 分类页 | 登录页 | 11 | | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | 12 | | ![首页](https://github.com/cnscorpions/xiaomi-shop-clone/blob/master/images/home.png) | ![分类页](https://github.com/cnscorpions/xiaomi-shop-clone/blob/master/images/category.png) | ![登录页](https://github.com/cnscorpions/xiaomi-shop-clone/blob/master/images/signup.png) | 13 | -------------------------------------------------------------------------------- /h5/src/router/router.config.js: -------------------------------------------------------------------------------- 1 | import Loadable from "react-loadable"; 2 | import Loading from "./Loading"; 3 | 4 | const Home = Loadable({ 5 | loader: () => import("pages/Home/Home"), 6 | loading: Loading, 7 | }); 8 | 9 | const Cart = Loadable({ 10 | loader: () => import("pages/Cart/Cart"), 11 | loading: Loading, 12 | }); 13 | 14 | const Category = Loadable({ 15 | loader: () => import("pages/Category/Category"), 16 | loading: Loading, 17 | }); 18 | 19 | const Star = Loadable({ 20 | loader: () => import("pages/Star/Star"), 21 | loading: Loading, 22 | }); 23 | 24 | const User = Loadable({ 25 | loader: () => import("pages/User/User"), 26 | loading: Loading, 27 | }); 28 | 29 | export const Routes = [ 30 | { 31 | path: "/", 32 | component: Home, 33 | }, 34 | { 35 | path: "/cart", 36 | component: Cart, 37 | }, 38 | { 39 | path: "/category", 40 | component: Category, 41 | }, 42 | { 43 | path: "/star", 44 | component: Star, 45 | }, 46 | { 47 | path: "/user", 48 | component: User, 49 | }, 50 | ]; 51 | -------------------------------------------------------------------------------- /h5/src/components/Banner/PaginationDot.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const styles = { 4 | root: { 5 | height: 18, 6 | width: 18, 7 | cursor: "pointer", 8 | border: 0, 9 | background: "none", 10 | padding: 0 11 | }, 12 | dot: { 13 | backgroundColor: "#ddd", 14 | height: ".8rem", 15 | width: ".8rem", 16 | borderRadius: ".4rem", 17 | margin: "0 .5rem" 18 | }, 19 | active: { 20 | backgroundColor: "#fff" 21 | } 22 | }; 23 | 24 | const PaginationDot = props => { 25 | const { active, onClick, index } = props; 26 | 27 | const handleClick = index => { 28 | onClick(index); 29 | }; 30 | 31 | let styleDot; 32 | 33 | if (active) { 34 | styleDot = Object.assign({}, styles.dot, styles.active); 35 | } else { 36 | styleDot = styles.dot; 37 | } 38 | 39 | return ( 40 | 47 | ); 48 | }; 49 | 50 | export default PaginationDot; 51 | -------------------------------------------------------------------------------- /weapp/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 |
    18 | 19 | 20 | -------------------------------------------------------------------------------- /h5/src/store/actionCreators/index.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actionTypes/types"; 2 | 3 | export const selectTabActionCreator = tab => ({ 4 | type: types.SELECT_TAB, 5 | payload: tab 6 | }); 7 | 8 | export const selectNavActionCreator = nav => ({ 9 | type: types.SET_NAV, 10 | payload: nav 11 | }); 12 | 13 | export const hideDownloadAppBanner = () => ({ 14 | type: types.HIDE_DOWNLOAD_APP_BANNER 15 | }); 16 | 17 | export const addSearchHistory = history => ({ 18 | type: types.ADD_SEARCH_HISTORY, 19 | payload: history 20 | }); 21 | 22 | export const removeAllSearchHistory = () => ({ 23 | type: types.REMOVE_ALL_SEARCH_HISTORY 24 | }); 25 | 26 | export const setCate = cate => ({ 27 | type: types.SET_CATE, 28 | payload: cate 29 | }); 30 | 31 | export const login = token => ({ 32 | type: types.LOGIN, 33 | payload: token 34 | }); 35 | 36 | export const addGoodsToCart = goods => ({ 37 | type: types.ADD_GOODS_TO_CART, 38 | payload: goods 39 | }); 40 | 41 | export const removeGoodsFromCart = goods => ({ 42 | type: types.REMOVE_GOODS_TO_CART, 43 | payload: goods 44 | }); 45 | -------------------------------------------------------------------------------- /weapp/.temp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 |
    18 | 19 | 20 | -------------------------------------------------------------------------------- /h5/src/pages/Cart/cart.module.scss: -------------------------------------------------------------------------------- 1 | @import "styles/global.scss"; 2 | 3 | .wrapper { 4 | padding-top: 5rem; 5 | .call-for-login { 6 | @include centerIt; 7 | justify-content: space-between; 8 | padding: 0 1.6rem; 9 | height: 5.4rem; 10 | &-left { 11 | display: block; 12 | font-size: 1.6rem; 13 | color: rgba(0, 0, 0, 0.87); 14 | } 15 | &-right { 16 | font-size: 1.25rem; 17 | color: rgba(0, 0, 0, 0.54); 18 | } 19 | } 20 | .empty-cart-container { 21 | @include centerIt; 22 | background: #ebebeb; 23 | padding: 1rem; 24 | &-icon { 25 | margin-right: 1rem; 26 | width: 4rem; 27 | } 28 | &-text { 29 | margin-right: 0.8rem; 30 | font-size: 1.25rem; 31 | color: rgba(0, 0, 0, 0.27); 32 | } 33 | &-btn { 34 | display: inline-block; 35 | border: 1px solid rgba(0, 0, 0, 0.15); 36 | box-sizing: border-box; 37 | height: 2.5rem; 38 | line-height: 2.5rem; 39 | padding: 0 1.25rem; 40 | color: rgba(0, 0, 0, 0.87); 41 | font-style: normal; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gavin Chan 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. 22 | -------------------------------------------------------------------------------- /h5/src/pages/Home/Header/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | import * as styles from "./header.module.scss"; 4 | 5 | import miIconPath from "assets/home/mi.png"; 6 | import userIconPath from "assets/tabbar/icon-user.png"; 7 | import searchIconPath from "assets/home/search.png"; 8 | 9 | export default function Header() { 10 | const history = useHistory(); 11 | 12 | const goToUser = () => { 13 | history.push("/user"); 14 | }; 15 | 16 | const goToSearchPage = () => { 17 | history.push("/search"); 18 | }; 19 | 20 | return ( 21 |
    22 |
    23 | 小米 24 |
    25 |
    29 | 搜索 30 | 搜索商品名称 31 |
    32 |
    33 | 用户 34 |
    35 |
    36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /weapp/src/app.jsx: -------------------------------------------------------------------------------- 1 | import Taro, { Component } from '@tarojs/taro' 2 | import { Provider } from '@tarojs/redux' 3 | 4 | import Index from './pages/index' 5 | 6 | import configStore from './store' 7 | 8 | import './app.scss' 9 | 10 | // 如果需要在 h5 环境中开启 React Devtools 11 | // 取消以下注释: 12 | // if (process.env.NODE_ENV !== 'production' && process.env.TARO_ENV === 'h5') { 13 | // require('nerv-devtools') 14 | // } 15 | 16 | const store = configStore() 17 | 18 | class App extends Component { 19 | 20 | config = { 21 | pages: [ 22 | 'pages/index/index' 23 | ], 24 | window: { 25 | backgroundTextStyle: 'light', 26 | navigationBarBackgroundColor: '#fff', 27 | navigationBarTitleText: 'WeChat', 28 | navigationBarTextStyle: 'black' 29 | } 30 | } 31 | 32 | componentDidMount () {} 33 | 34 | componentDidShow () {} 35 | 36 | componentDidHide () {} 37 | 38 | componentDidCatchError () {} 39 | 40 | // 在 App 类中的 render() 函数没有实际作用 41 | // 请勿修改此函数 42 | render () { 43 | return ( 44 | 45 | 46 | 47 | ) 48 | } 49 | } 50 | 51 | Taro.render(, document.getElementById('app')) 52 | -------------------------------------------------------------------------------- /h5/src/pages/Home/Content/content.config.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Loadable from "react-loadable"; 3 | import Others from "pages/Home/Content/Others/Others"; 4 | 5 | const Loading = () =>
    加载中...
    ; 6 | 7 | const Recommend = Loadable({ 8 | loader: () => import("pages/Home/Content/Recommend/Recommend"), 9 | loading: Loading 10 | }); 11 | 12 | const Cellphone = Loadable({ 13 | loader: () => import("pages/Home/Content/Cellphone/Cellphone"), 14 | loading: Loading 15 | }); 16 | 17 | const SmartDevice = Loadable({ 18 | loader: () => import("pages/Home/Content/SmartDevice/SmartDevice"), 19 | loading: Loading 20 | }); 21 | 22 | const TV = Loadable({ 23 | loader: () => import("pages/Home/Content/TV/TV"), 24 | loading: Loading 25 | }); 26 | 27 | const Laptop = Loadable({ 28 | loader: () => import("pages/Home/Content/Laptop/Laptop"), 29 | loading: Loading 30 | }); 31 | 32 | const Appliance = Loadable({ 33 | loader: () => import("pages/Home/Content/Appliance/Appliance"), 34 | loading: Loading 35 | }); 36 | 37 | export const ContentConfig = { 38 | recommend: Recommend, 39 | cellphone: Cellphone, 40 | smartDevice: SmartDevice, 41 | tv: TV, 42 | laptop: Laptop, 43 | appliance: Appliance, 44 | others: Others 45 | }; 46 | -------------------------------------------------------------------------------- /h5/src/pages/Login/login.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | padding: 0 2rem 5rem; 3 | .header-container { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | padding-top: 3rem; 9 | padding-bottom: 1rem; 10 | & > img { 11 | width: 4.8rem; 12 | } 13 | & > h4 { 14 | margin-top: 1rem; 15 | font-size: 1.8rem; 16 | color: #000; 17 | font-weight: normal; 18 | } 19 | } 20 | .login-form { 21 | position: relative; 22 | display: flex; 23 | flex-direction: column; 24 | & > input { 25 | margin-bottom: 1rem; 26 | padding: 1.6rem 0; 27 | border: none; 28 | background: none !important; 29 | font-size: 1.8rem; 30 | } 31 | & > input:focus { 32 | outline: none; 33 | } 34 | .login-form-btn { 35 | margin-bottom: 1.4rem; 36 | padding: 1rem 0; 37 | width: 100%; 38 | text-align: center; 39 | border: none; 40 | border-radius: 0.6rem; 41 | overflow: hidden; 42 | background-color: #ff6700 !important; 43 | font-size: 1.8rem; 44 | color: #fff; 45 | } 46 | .icon-eye { 47 | position: absolute; 48 | right: 0.2rem; 49 | bottom: 8rem; 50 | width: 2rem; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /h5/src/router/AppRouter.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; 3 | import PrivateRoute from "./PrivateRoute"; 4 | import { Routes } from "./router.config"; 5 | 6 | import Login from "pages/Login/Login"; 7 | import AppLayout from "components/Layout/AppLayout"; 8 | import NotFound from "pages/NotFound/NotFound"; 9 | import LandingPage from "pages/LandingPage/LandingPage"; 10 | import Search from "pages/Search/Search"; 11 | import List from "pages/List/List"; 12 | 13 | export default function AppRouter(props) { 14 | return ( 15 | 16 | 17 | {Routes.map((route, index) => ( 18 | 19 | } /> 20 | 21 | ))} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /server/src/api/routes/auth.ts: -------------------------------------------------------------------------------- 1 | import { Router, Request, Response, NextFunction } from 'express'; 2 | import { celebrate, Joi } from 'celebrate'; 3 | import logger from '../../loaders/logger'; 4 | import AuthService from '../../services/auth.service'; 5 | import { UserDTO } from '../../interfaces/IUser'; 6 | 7 | const router = Router(); 8 | 9 | export default (app: Router) => { 10 | // 挂在当前路由模块到主应用的/auth路径下 11 | app.use('/auth', router); 12 | 13 | // 登录/注册 14 | router.post( 15 | '/verify', 16 | celebrate({ 17 | body: Joi.object({ 18 | email: Joi.string().required(), 19 | password: Joi.string().required(), 20 | }), 21 | }), 22 | async (req: Request, res: Response, next: NextFunction) => { 23 | logger.debug('Calling Sign-In endpoint with body: %o', req.body); 24 | try { 25 | const { email, password } = req.body; 26 | const authService = new AuthService(); 27 | const { user, token, message } = await authService.signIn(email, password); 28 | return res.status(200).json({ user, token, message }); 29 | } catch (error) { 30 | logger.error('🔥 error: %o', error); 31 | return next(error); 32 | } 33 | }, 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /weapp/src/pages/index/index.jsx: -------------------------------------------------------------------------------- 1 | import Taro, { Component } from '@tarojs/taro' 2 | import { View, Button, Text } from '@tarojs/components' 3 | import { connect } from '@tarojs/redux' 4 | 5 | import { add, minus, asyncAdd } from '../../actions/counter' 6 | 7 | import './index.scss' 8 | 9 | 10 | @connect(({ counter }) => ({ 11 | counter 12 | }), (dispatch) => ({ 13 | add () { 14 | dispatch(add()) 15 | }, 16 | dec () { 17 | dispatch(minus()) 18 | }, 19 | asyncAdd () { 20 | dispatch(asyncAdd()) 21 | } 22 | })) 23 | class Index extends Component { 24 | 25 | config = { 26 | navigationBarTitleText: '首页' 27 | } 28 | 29 | componentWillReceiveProps (nextProps) { 30 | console.log(this.props, nextProps) 31 | } 32 | 33 | componentWillUnmount () { } 34 | 35 | componentDidShow () { } 36 | 37 | componentDidHide () { } 38 | 39 | render () { 40 | return ( 41 | 42 | 43 | 44 | 45 | {this.props.counter.num} 46 | Hello, World 47 | 48 | ) 49 | } 50 | } 51 | 52 | export default Index 53 | -------------------------------------------------------------------------------- /h5/src/pages/Star/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | 4 | import bellIcon from "assets/star/message-bell.png"; 5 | import userIcon from "assets/star/user.png"; 6 | 7 | const styles = { 8 | wrapper: { 9 | display: "flex", 10 | flexWrap: "wrap", 11 | justifyContent: "space-between", 12 | alignItems: "center", 13 | padding: "0 1.25rem", 14 | height: "4.3rem", 15 | }, 16 | item: { 17 | display: "block", 18 | padding: "0 1rem", 19 | height: "4.3rem", 20 | lineHeight: "4.3rem", 21 | fontSize: "1.7rem", 22 | fontWeight: 700, 23 | }, 24 | iconBell: { 25 | width: "4.1rem", 26 | height: "auto", 27 | }, 28 | iconUser: { 29 | width: "2.7rem", 30 | height: "2.7rem", 31 | }, 32 | }; 33 | 34 | export default function Header() { 35 | const history = useHistory(); 36 | 37 | const goToLogin = () => { 38 | history.push("/login"); 39 | }; 40 | 41 | return ( 42 |
    43 | 推荐 44 | 小米智能生活 45 | 5G讨论组 46 | 47 | 53 |
    54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /weapp/.temp/pages/index/index.js: -------------------------------------------------------------------------------- 1 | import Nerv from "nervjs"; 2 | import Taro from "@tarojs/taro-h5"; 3 | import { View, Button, Text } from '@tarojs/components'; 4 | import { connect } from "@tarojs/redux-h5"; 5 | 6 | import { add, minus, asyncAdd } from '../../actions/counter'; 7 | 8 | import './index.scss'; 9 | 10 | @connect(({ counter }) => ({ 11 | counter 12 | }), dispatch => ({ 13 | add() { 14 | dispatch(add()); 15 | }, 16 | dec() { 17 | dispatch(minus()); 18 | }, 19 | asyncAdd() { 20 | dispatch(asyncAdd()); 21 | } 22 | })) 23 | class Index extends Taro.Component { 24 | 25 | config = { 26 | navigationBarTitleText: '首页' 27 | }; 28 | 29 | componentWillReceiveProps(nextProps) { 30 | console.log(this.props, nextProps); 31 | } 32 | 33 | componentWillUnmount() {} 34 | 35 | componentDidShow() {} 36 | 37 | componentDidHide() {} 38 | 39 | render() { 40 | return 41 | 42 | 43 | 44 | {this.props.counter.num} 45 | Hello, World 46 | ; 47 | } 48 | 49 | componentDidMount() { 50 | super.componentDidMount && super.componentDidMount(); 51 | } 52 | 53 | } 54 | 55 | export default Index; -------------------------------------------------------------------------------- /h5/src/pages/Category/RightPanel/rightPanel.module.scss: -------------------------------------------------------------------------------- 1 | @import "styles/global.scss"; 2 | 3 | .wrapper { 4 | padding-left: 9.5rem; 5 | padding-right: 1.6rem; 6 | scroll-behavior: smooth; // 滚动条缓慢滚动 7 | .banner-img { 8 | width: 26rem; 9 | height: auto; 10 | } 11 | .section-title { 12 | @include centerIt; 13 | background: #fff; 14 | font-size: 1.4rem; 15 | text-align: center; 16 | font-weight: 400; 17 | margin-top: 1rem; 18 | height: 6.6rem; 19 | height: 6.6rem; 20 | overflow: hidden; 21 | } 22 | .section-title > span { 23 | position: relative; 24 | } 25 | .section-title > span::before { 26 | position: absolute; 27 | top: 50%; 28 | left: 0; 29 | width: 2rem; 30 | border-top: 1px solid #e0e0e0; 31 | content: ""; 32 | transform: translate3d(-150%, -50%, 0); 33 | } 34 | .section-title > span::after { 35 | position: absolute; 36 | top: 50%; 37 | left: auto; 38 | right: 0; 39 | width: 2rem; 40 | border-top: 1px solid #e0e0e0; 41 | content: ""; 42 | transform: translate3d(150%, -50%, 0); 43 | } 44 | .product-container { 45 | display: flex; 46 | flex-wrap: wrap; 47 | .product-box { 48 | @include centerIt; 49 | flex-direction: column; 50 | margin: 1rem 0 1.5rem 0; 51 | width: 33.3%; 52 | &-img { 53 | margin-bottom: 1.5rem; 54 | width: 5.2rem; 55 | height: auto; 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /server/src/loaders/server.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response, NextFunction } from 'express'; 2 | import bodyParser from 'body-parser'; 3 | import cors from 'cors'; 4 | import config from '../config'; 5 | import routes from '../api'; 6 | 7 | export default ({ app }: { app: express.Application }) => { 8 | // 测试endpoint api 9 | app.get('/', (req: Request, res: Response) => { 10 | res.send('endpoint works!'); 11 | }); 12 | 13 | // 解决前端跨域问题 14 | app.use(cors()); 15 | 16 | // 请求体转换为json格式 17 | app.use(bodyParser.json()); 18 | 19 | // 加载api 20 | app.use(config.api.prefix, routes()); 21 | 22 | // 捕获404,转发给error handler 23 | app.use((req, res, next) => { 24 | const err: any = new Error('Not Found'); 25 | err['status'] = 404; 26 | next(err); 27 | }); 28 | 29 | /// error handlers 30 | app.use((err: any, req: Request, res: Response, next: NextFunction) => { 31 | /** 32 | * Handle 401 thrown by express-jwt library 33 | */ 34 | if (err.name === 'UnauthorizedError') { 35 | return res.status(err.status).send({ message: err.message }).end(); 36 | } 37 | return next(err); 38 | }); 39 | app.use((err: any, req: Request, res: Response, next: NextFunction) => { 40 | res.status(err.status || 500); 41 | res.json({ 42 | errors: { 43 | message: err.message, 44 | }, 45 | }); 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /h5/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 17 | React App 18 | 19 | 39 | 40 | 41 | 42 |
    43 | 44 | 45 | -------------------------------------------------------------------------------- /server/src/config/index.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | // Set the NODE_ENV to 'development' by default 4 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 5 | 6 | const envFound = dotenv.config(); 7 | 8 | if (envFound.error) { 9 | // This error should crash whole process 10 | throw new Error("⚠️ Couldn't find .env file ⚠️"); 11 | } 12 | 13 | export default { 14 | /** 15 | * serve port 16 | */ 17 | port: parseInt(process.env.PORT, 10), 18 | 19 | /** 20 | * database url 21 | */ 22 | databaseURL: process.env.MONGODB_URI, 23 | 24 | /** 25 | * secret 26 | */ 27 | jwtSecret: process.env.JWT_SECRET, 28 | 29 | /** 30 | * Used by winston logger 31 | */ 32 | logs: { 33 | level: process.env.LOG_LEVEL || 'silly', 34 | }, 35 | 36 | /** 37 | * Agenda.js stuff 38 | */ 39 | agenda: { 40 | dbCollection: process.env.AGENDA_DB_COLLECTION, 41 | pooltime: process.env.AGENDA_POOL_TIME, 42 | concurrency: parseInt(process.env.AGENDA_CONCURRENCY, 10), 43 | }, 44 | 45 | /** 46 | * Agendash config 47 | */ 48 | agendash: { 49 | user: 'agendash', 50 | password: '123456', 51 | }, 52 | /** 53 | * API configs 54 | */ 55 | api: { 56 | prefix: '/api', 57 | }, 58 | /** 59 | * Mailgun email credentials 60 | */ 61 | emails: { 62 | apiKey: process.env.MAILGUN_API_KEY, 63 | domain: process.env.MAILGUN_DOMAIN, 64 | }, 65 | }; 66 | -------------------------------------------------------------------------------- /h5/src/components/Banner/Banner.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import SwipeableViews from "react-swipeable-views"; 3 | // import { autoPlay } from "react-swipeable-views-utils"; 4 | import Pagination from "./Pagination"; 5 | 6 | import banner01 from "assets/home/banner01.jpg"; 7 | import banner02 from "assets/home/banner02.jpg"; 8 | import banner03 from "assets/home/banner03.jpg"; 9 | 10 | // autoplay banner 11 | // const AutoPlaySwipeableViews = autoPlay(SwipeableViews); 12 | 13 | const styles = { 14 | container: { 15 | position: "relative", 16 | }, 17 | slide: { 18 | minHeight: "18.8rem", 19 | color: "#fff", 20 | backgroundImage: 'url("")', 21 | backgroundSize: "contain", 22 | backgroundPosition: "center center", 23 | backgroundRepeat: "no-repeat", 24 | }, 25 | slide1: { 26 | backgroundImage: `url(${banner01})`, 27 | }, 28 | slide2: { 29 | backgroundImage: `url(${banner02})`, 30 | }, 31 | slide3: { 32 | backgroundImage: `url(${banner03})`, 33 | }, 34 | }; 35 | 36 | export default function Banner() { 37 | const [index, setIndex] = useState(0); 38 | 39 | const handleChangeIndex = (index) => { 40 | setIndex(index); 41 | }; 42 | 43 | return ( 44 |
    45 | 46 |
    47 |
    48 |
    49 | 50 | 51 |
    52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /h5/src/pages/Home/Content/Recommend/Recommend.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Banner from "components/Banner/Banner"; 3 | import Cate from "components/Cate/Cate"; 4 | 5 | import cate01 from "assets/home/cate/小米秒杀.png"; 6 | import cate02 from "assets/home/cate/小米众筹.png"; 7 | import cate03 from "assets/home/cate/K30-Pro.png"; 8 | import cate04 from "assets/home/cate/以旧换新.png"; 9 | import cate05 from "assets/home/cate/超值特卖.png"; 10 | import cate06 from "assets/home/cate/智能.png"; 11 | import cate07 from "assets/home/cate/笔记本热卖.png"; 12 | import cate08 from "assets/home/cate/电视热卖.png"; 13 | import cate09 from "assets/home/cate/洗衣机热卖.png"; 14 | import cate10 from "assets/home/cate/米粉卡.png"; 15 | 16 | const cates = [ 17 | { 18 | text: "小米秒杀", 19 | key: "seckill", 20 | icon: cate01 21 | }, 22 | { 23 | text: "小米众筹", 24 | key: "crowd", 25 | icon: cate02 26 | }, 27 | { 28 | text: "K30-Pro", 29 | key: "miproduct", 30 | icon: cate03 31 | }, 32 | { 33 | text: "以旧换新", 34 | key: "recycling", 35 | icon: cate04 36 | }, 37 | { 38 | text: "超值特卖", 39 | key: "bargain", 40 | icon: cate05 41 | }, 42 | { 43 | text: "智能", 44 | key: "smart", 45 | icon: cate06 46 | }, 47 | { 48 | text: "笔记本热卖", 49 | key: "laptop", 50 | icon: cate07 51 | }, 52 | { 53 | text: "电视热卖", 54 | key: "tv", 55 | icon: cate08 56 | }, 57 | { 58 | text: "洗衣机热卖", 59 | key: "laundry", 60 | icon: cate09 61 | }, 62 | { 63 | text: "米粉卡", 64 | key: "sim", 65 | icon: cate10 66 | } 67 | ]; 68 | 69 | export default function Recommend() { 70 | return ( 71 | <> 72 | 73 | 74 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /h5/src/components/DownloadApp/DownloadApp.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | import { useSelector, useDispatch } from "react-redux"; 4 | import * as actionCreators from "store/actionCreators/index"; 5 | import DownloadAppImg from "assets/common/下载app.png"; 6 | 7 | const styles = { 8 | container: { 9 | position: "relative", 10 | height: "5.2rem", 11 | overflow: "hidden", 12 | }, 13 | img: { 14 | display: "block", 15 | width: "100%", 16 | height: "auto", 17 | }, 18 | hide: { 19 | display: "none", 20 | }, 21 | close: { 22 | position: "absolute", 23 | left: 0, 24 | top: 0, 25 | display: "inline-block", 26 | width: "5.2rem", 27 | height: "5.2rem", 28 | background: "transparent", 29 | }, 30 | goTo: { 31 | position: "absolute", 32 | right: 0, 33 | top: 0, 34 | display: "inline-block", 35 | width: "10rem", 36 | height: "5.2rem", 37 | background: "transparent", 38 | }, 39 | }; 40 | 41 | export default function DownloadApp() { 42 | const history = useHistory(); 43 | const selector = useSelector((state) => state.common.isShowDownloadAppBanner); 44 | const dispatch = useDispatch(); 45 | 46 | const goToLandingPage = () => { 47 | history.push("/download"); 48 | }; 49 | 50 | const closeBanner = () => { 51 | const action = actionCreators.hideDownloadAppBanner(); 52 | dispatch(action); 53 | }; 54 | 55 | return ( 56 |
    57 | 58 | 59 | 60 |
    61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /h5/src/styles/reset.scss: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, 7 | body, 8 | div, 9 | span, 10 | applet, 11 | object, 12 | iframe, 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6, 19 | p, 20 | blockquote, 21 | pre, 22 | a, 23 | abbr, 24 | acronym, 25 | address, 26 | big, 27 | cite, 28 | code, 29 | del, 30 | dfn, 31 | em, 32 | img, 33 | ins, 34 | kbd, 35 | q, 36 | s, 37 | samp, 38 | small, 39 | strike, 40 | strong, 41 | sub, 42 | sup, 43 | tt, 44 | var, 45 | b, 46 | u, 47 | i, 48 | center, 49 | dl, 50 | dt, 51 | dd, 52 | ol, 53 | ul, 54 | li, 55 | fieldset, 56 | form, 57 | label, 58 | legend, 59 | table, 60 | caption, 61 | tbody, 62 | tfoot, 63 | thead, 64 | tr, 65 | th, 66 | td, 67 | article, 68 | aside, 69 | canvas, 70 | details, 71 | embed, 72 | figure, 73 | figcaption, 74 | footer, 75 | header, 76 | hgroup, 77 | menu, 78 | nav, 79 | output, 80 | ruby, 81 | section, 82 | summary, 83 | time, 84 | mark, 85 | audio, 86 | video { 87 | margin: 0; 88 | padding: 0; 89 | border: 0; 90 | font-size: 100%; 91 | font: inherit; 92 | vertical-align: baseline; 93 | } 94 | /* HTML5 display-role reset for older browsers */ 95 | article, 96 | aside, 97 | details, 98 | figcaption, 99 | figure, 100 | footer, 101 | header, 102 | hgroup, 103 | menu, 104 | nav, 105 | section { 106 | display: block; 107 | } 108 | body { 109 | line-height: 1; 110 | } 111 | ol, 112 | ul { 113 | list-style: none; 114 | } 115 | blockquote, 116 | q { 117 | quotes: none; 118 | } 119 | blockquote:before, 120 | blockquote:after, 121 | q:before, 122 | q:after { 123 | content: ""; 124 | content: none; 125 | } 126 | table { 127 | border-collapse: collapse; 128 | border-spacing: 0; 129 | } 130 | -------------------------------------------------------------------------------- /h5/src/pages/Search/Header/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | import { useDispatch } from "react-redux"; 4 | import * as styles from "./header.module.scss"; 5 | 6 | import * as actionCreators from "store/actionCreators/index"; 7 | 8 | import arrowLeftIcon from "assets/search/arrow-left.png"; 9 | import searchIconPath from "assets/home/search.png"; 10 | 11 | export default function Header() { 12 | const [inputValue, setInputValue] = useState(""); 13 | const dispatch = useDispatch(); 14 | 15 | const history = useHistory(); 16 | 17 | const goBack = () => { 18 | history.goBack(); 19 | }; 20 | 21 | const handleInputChange = event => { 22 | const { value } = event.currentTarget; 23 | setInputValue(value); 24 | }; 25 | 26 | const handleKeyEnter = event => { 27 | if (event.key === "Enter") { 28 | goToList(); 29 | } 30 | }; 31 | 32 | const goToList = () => { 33 | if (!inputValue) return false; 34 | history.push(`/list?keyword=${encodeURI(inputValue)}`); 35 | addSearchHistory(inputValue); 36 | }; 37 | 38 | const addSearchHistory = history => { 39 | const action = actionCreators.addSearchHistory(history); 40 | dispatch(action); 41 | }; 42 | 43 | return ( 44 |
    45 |
    46 | 返回上一页 47 |
    48 | 55 |
    56 | 搜索 57 |
    58 |
    59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /h5/src/pages/Home/Navbar/navbar.module.scss: -------------------------------------------------------------------------------- 1 | @import "styles/global.scss"; 2 | 3 | .container { 4 | position: relative; 5 | z-index: 999; 6 | } 7 | 8 | .nav-wrapper { 9 | display: flex; 10 | flex-direction: row; 11 | flex-wrap: nowrap; 12 | width: 100%; 13 | height: 3rem; 14 | background: #f2f2f2; 15 | .nav-item { 16 | flex: 1; 17 | @include centerIt; 18 | color: rgb(116, 116, 116); 19 | border-color: rgb(242, 242, 242); 20 | font-size: 1.4rem; 21 | &-text { 22 | display: block; 23 | height: 3rem; 24 | line-height: 3rem; 25 | } 26 | } 27 | .nav-collapse { 28 | @include centerIt; 29 | flex: 1; 30 | } 31 | .nav-collapse > img { 32 | width: 1.6rem; 33 | } 34 | .active { 35 | color: rgb(237, 91, 0); 36 | border-bottom: 2px solid rgb(237, 91, 0); 37 | } 38 | } 39 | 40 | .nav-layer { 41 | position: absolute; 42 | top: 0; 43 | width: 100%; 44 | box-sizing: border-box; 45 | background-color: #eee; 46 | font-size: 1.5rem; 47 | text-align: left; 48 | padding: 0 1.3rem; 49 | transition: max-height 0.3s ease-in-out; 50 | overflow: hidden; 51 | &-title { 52 | padding-top: 1.4rem; 53 | } 54 | &-arrow-up { 55 | position: absolute; 56 | right: 1.6rem; 57 | top: 0.8rem; 58 | width: 1.6rem; 59 | } 60 | &-btn-group { 61 | display: flex; 62 | justify-content: flex-start; 63 | flex-wrap: wrap; 64 | margin-top: 1.7rem; 65 | &-btn { 66 | display: inline-block; 67 | margin: 0 0.6rem 1rem 0; 68 | width: 7.7rem; 69 | height: 2.7rem; 70 | text-align: center; 71 | line-height: 2.7rem; 72 | border: 1px solid #e5e5e5; 73 | border-radius: 0.4rem; 74 | font-size: 1.3rem; 75 | background-color: #fff; 76 | } 77 | &-btn_active { 78 | background-color: #fde0d5; 79 | border-color: #ff6700; 80 | color: #ff6700; 81 | } 82 | } 83 | } 84 | 85 | .hide { 86 | display: none; 87 | } 88 | -------------------------------------------------------------------------------- /h5/src/pages/Cart/Cart.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | import { useSelector } from "react-redux"; 4 | import Header from "components/Header/Header"; 5 | 6 | import * as styles from "./cart.module.scss"; 7 | import cartIcon from "assets/cart/cart.png"; 8 | 9 | const CallForLogin = (props) => { 10 | const { goLogin } = props; 11 | 12 | return ( 13 |
    14 | 登录后享受更多优惠 15 | 去登录 16 |
    17 | ); 18 | }; 19 | 20 | const EmptyCart = (props) => { 21 | const { goHome } = props; 22 | 23 | return ( 24 |
    25 | icon 30 | 31 | 购物车还是空的 32 | 33 | 34 | 去逛逛 35 | 36 |
    37 | ); 38 | }; 39 | 40 | export default function Cart() { 41 | const history = useHistory(); 42 | const isAuthed = useSelector((state) => state.auth.isAuthed); 43 | const goods = useSelector((state) => state.cart.goods); 44 | 45 | const goBack = () => { 46 | history.goBack(); 47 | }; 48 | 49 | const goSearch = () => { 50 | history.push("/search"); 51 | }; 52 | 53 | const goLogin = () => { 54 | history.push("/login"); 55 | }; 56 | 57 | const goHome = () => { 58 | history.push("/"); 59 | }; 60 | 61 | return ( 62 |
    63 |
    64 | {isAuthed ? null : } 65 | {goods.length > 0 ? null : } 66 |
    67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mi-shop-backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.ts", 6 | "scripts": { 7 | "dev": "set DEBUG=express:* && ts-node-dev --no-notify src/app.ts", 8 | "serve": "tsc && node dist/app.js", 9 | "watch": "tsc -w", 10 | "build": "npm run lint && rimraf dist && tsc", 11 | "lint": "eslint --ext js,ts src/* --quiet --fix", 12 | "git": "git add . && git commit -m", 13 | "postgit": "git pull && git push && git status" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "dependencies": { 18 | "@types/agenda": "^2.0.8", 19 | "@types/cors": "^2.8.6", 20 | "@types/express": "^4.17.6", 21 | "@types/hapi__joi": "^17.1.0", 22 | "@types/jsonwebtoken": "^8.5.0", 23 | "@types/mongoose": "^5.7.21", 24 | "@types/node": "^14.0.5", 25 | "agenda": "^3.1.0", 26 | "body-parser": "^1.19.0", 27 | "celebrate": "^12.1.1", 28 | "cors": "^2.8.5", 29 | "dotenv": "^8.2.0", 30 | "express": "^4.17.1", 31 | "husky": "^4.2.5", 32 | "jsonwebtoken": "^8.5.1", 33 | "lint-staged": "^10.2.6", 34 | "mongoose": "^5.9.16", 35 | "reflect-metadata": "^0.1.13", 36 | "typedi": "^0.8.0", 37 | "winston": "^3.2.1" 38 | }, 39 | "devDependencies": { 40 | "@typescript-eslint/eslint-plugin": "^3.0.1", 41 | "@typescript-eslint/parser": "^3.0.1", 42 | "eslint": "^7.1.0", 43 | "eslint-config-prettier": "^6.11.0", 44 | "eslint-plugin-prettier": "^3.1.3", 45 | "prettier": "^2.0.5", 46 | "ts-node-dev": "^1.0.0-pre.44", 47 | "typescript": "^3.9.3" 48 | }, 49 | "husky": { 50 | "hooks": { 51 | "pre-commit": "lint-staged" 52 | } 53 | }, 54 | "lint-staged": { 55 | "*.{js,ts,tsx}": [ 56 | "eslint --fix" 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /h5/src/pages/Search/History.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import * as actionCreators from "store/actionCreators/index"; 4 | import deleteIcon from "assets/search/delete.png"; 5 | 6 | const styles = { 7 | wrapper: { 8 | marginBottom: "3.2rem", 9 | padding: "2rem 1.6rem", 10 | color: "#3C3C3C" 11 | }, 12 | header: { 13 | display: "flex", 14 | justifyContent: "space-between" 15 | }, 16 | title: { 17 | fontSize: "1.3rem", 18 | fontWeight: 700 19 | }, 20 | icon: { 21 | width: "1.4rem", 22 | height: "1.4rem" 23 | }, 24 | historyContainer: { 25 | display: "flex", 26 | margin: ".4rem 1.2rem 0 0" 27 | }, 28 | tag: { 29 | display: "flex", 30 | justifyContent: "center", 31 | alignItems: "center", 32 | minWidth: "5rem", 33 | boxSizing: "border-box", 34 | margin: "1.2rem 1.2rem 0 0", 35 | padding: ".8rem 1.2rem", 36 | minHeight: "3rem", 37 | borderRadius: "2.8rem", 38 | overflow: "hidden", 39 | whiteSpace: "nowrap", 40 | textOverflow: "ellipsis", 41 | background: "rgba(0,0,0,.04)" 42 | } 43 | }; 44 | 45 | export default function History() { 46 | const tags = useSelector(state => state.search.historyList); 47 | const dispatch = useDispatch(); 48 | 49 | const removeAllHistory = () => { 50 | const action = actionCreators.removeAllSearchHistory(); 51 | dispatch(action); 52 | }; 53 | 54 | return ( 55 |
    56 |
    57 | 搜索历史 58 | 删除 64 |
    65 |
    66 | {tags.map((tag, index) => ( 67 | 68 | {tag} 69 | 70 | ))} 71 |
    72 |
    73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /h5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "h5", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "husky": "^4.2.5", 10 | "lint-staged": "^10.2.6", 11 | "node-sass": "^4.14.1", 12 | "prettier": "^2.0.5", 13 | "react": "^16.13.1", 14 | "react-dom": "^16.13.1", 15 | "react-hook-form": "^5.7.2", 16 | "react-loadable": "github:p0o/react-loadable", 17 | "react-redux": "^7.2.0", 18 | "react-router-dom": "^5.1.2", 19 | "react-scripts": "3.4.1", 20 | "react-spinners": "^0.8.3", 21 | "react-swipeable-views": "^0.13.9", 22 | "redux": "^4.0.5", 23 | "redux-persist": "^6.0.0", 24 | "source-map-explorer": "^2.4.2" 25 | }, 26 | "scripts": { 27 | "git": "git add . && git commit -m", 28 | "postgit": "git pull && git push && git status", 29 | "start": "react-scripts start", 30 | "build": "react-scripts build", 31 | "lint": "eslint --ext js,ts src/* --quiet --fix", 32 | "analyze": "source-map-explorer 'build/static/js/*.js'", 33 | "test": "react-scripts test", 34 | "format": "prettier --write 'src/**/*.{js,jsx,ts,tsx,json,css,scss,md}'", 35 | "eject": "react-scripts eject" 36 | }, 37 | "eslintConfig": { 38 | "extends": "react-app" 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | }, 52 | "devDependencies": { 53 | "eslint-config-airbnb": "^18.1.0", 54 | "eslint-config-prettier": "^6.11.0", 55 | "eslint-plugin-jsx-a11y": "^6.2.3", 56 | "eslint-plugin-prettier": "^3.1.3" 57 | }, 58 | "husky": { 59 | "hooks": { 60 | "pre-commit": "lint-staged" 61 | } 62 | }, 63 | "lint-staged": { 64 | "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [ 65 | "prettier --write" 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /weapp/.temp/app.js: -------------------------------------------------------------------------------- 1 | import Taro, { Component } from "@tarojs/taro-h5"; 2 | import { Provider } from "@tarojs/redux-h5"; 3 | 4 | import configStore from "./store/index"; 5 | 6 | import './app.scss'; 7 | 8 | // 如果需要在 h5 环境中开启 React Devtools 9 | // 取消以下注释: 10 | // if (process.env.NODE_ENV !== 'production' && process.env.TARO_ENV === 'h5') { 11 | // require('nerv-devtools') 12 | // } 13 | 14 | import Nerv from 'nervjs'; 15 | import { Router, createHistory, mountApis } from '@tarojs/router'; 16 | Taro.initPxTransform({ 17 | "designWidth": 750, 18 | "deviceRatio": { 19 | "640": 1.17, 20 | "750": 1, 21 | "828": 0.905 22 | } 23 | }); 24 | 25 | const _taroHistory = createHistory({ 26 | mode: "hash", 27 | basename: "/", 28 | customRoutes: {}, 29 | firstPagePath: "/pages/index/index" 30 | }); 31 | 32 | mountApis({ 33 | "basename": "/", 34 | "customRoutes": {} 35 | }, _taroHistory); 36 | const store = configStore(); 37 | 38 | class App extends Component { 39 | 40 | config = { 41 | pages: ["/pages/index/index"], 42 | window: { 43 | backgroundTextStyle: 'light', 44 | navigationBarBackgroundColor: '#fff', 45 | navigationBarTitleText: 'WeChat', 46 | navigationBarTextStyle: 'black' 47 | } 48 | }; 49 | 50 | componentDidMount() { 51 | this.componentDidShow(); 52 | } 53 | 54 | componentDidShow() {} 55 | 56 | componentDidHide() {} 57 | 58 | componentDidCatchError() {} 59 | 60 | // 在 App 类中的 render() 函数没有实际作用 61 | // 请勿修改此函数 62 | render() { 63 | return 64 | 65 | import( /* webpackChunkName: "index_index" */'./pages/index/index'), 68 | isIndex: true 69 | }]} customRoutes={{}} /> 70 | 71 | ; 72 | } 73 | 74 | componentWillUnmount() { 75 | this.componentDidHide(); 76 | } 77 | 78 | constructor(props, context) { 79 | super(props, context); 80 | Taro._$app = this; 81 | } 82 | 83 | } 84 | 85 | Nerv.render(, document.getElementById('app')); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /weapp/config/index.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | projectName: 'weapp', 3 | date: '2020-6-5', 4 | designWidth: 750, 5 | deviceRatio: { 6 | '640': 2.34 / 2, 7 | '750': 1, 8 | '828': 1.81 / 2 9 | }, 10 | sourceRoot: 'src', 11 | outputRoot: 'dist', 12 | babel: { 13 | sourceMap: true, 14 | presets: [ 15 | ['env', { 16 | modules: false 17 | }] 18 | ], 19 | plugins: [ 20 | 'transform-decorators-legacy', 21 | 'transform-class-properties', 22 | 'transform-object-rest-spread', 23 | ['transform-runtime', { 24 | helpers: false, 25 | polyfill: false, 26 | regenerator: true, 27 | moduleName: 'babel-runtime' 28 | } 29 | ] 30 | ] 31 | }, 32 | defineConstants: { 33 | }, 34 | mini: { 35 | postcss: { 36 | autoprefixer: { 37 | enable: true, 38 | config: { 39 | browsers: [ 40 | 'last 3 versions', 41 | 'Android >= 4.1', 42 | 'ios >= 8' 43 | ] 44 | } 45 | }, 46 | pxtransform: { 47 | enable: true, 48 | config: { 49 | 50 | } 51 | }, 52 | url: { 53 | enable: true, 54 | config: { 55 | limit: 10240 // 设定转换尺寸上限 56 | } 57 | }, 58 | cssModules: { 59 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true 60 | config: { 61 | namingPattern: 'module', // 转换模式,取值为 global/module 62 | generateScopedName: '[name]__[local]___[hash:base64:5]' 63 | } 64 | } 65 | } 66 | }, 67 | h5: { 68 | publicPath: '/', 69 | staticDirectory: 'static', 70 | postcss: { 71 | autoprefixer: { 72 | enable: true, 73 | config: { 74 | browsers: [ 75 | 'last 3 versions', 76 | 'Android >= 4.1', 77 | 'ios >= 8' 78 | ] 79 | } 80 | }, 81 | cssModules: { 82 | enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true 83 | config: { 84 | namingPattern: 'module', // 转换模式,取值为 global/module 85 | generateScopedName: '[name]__[local]___[hash:base64:5]' 86 | } 87 | } 88 | } 89 | } 90 | } 91 | 92 | module.exports = function (merge) { 93 | if (process.env.NODE_ENV === 'development') { 94 | return merge({}, config, require('./dev')) 95 | } 96 | return merge({}, config, require('./prod')) 97 | } 98 | -------------------------------------------------------------------------------- /h5/src/components/Tabbar/Tabbar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { useSelector, useDispatch } from "react-redux"; 4 | import * as actionCreators from "store/actionCreators/index"; 5 | 6 | import * as styles from "./tabbar.module.scss"; 7 | 8 | import homeIconPath from "assets/tabbar/icon-home.png"; 9 | import homeActiveIconPath from "assets/tabbar/icon-home-selected.png"; 10 | import categoryIconPath from "assets/tabbar/icon-category.png"; 11 | import categoryActiveIconPath from "assets/tabbar/icon-category-selected.png"; 12 | import starIconPath from "assets/tabbar/icon-star.png"; 13 | import starActiveIconPath from "assets/tabbar/icon-star-selected.png"; 14 | import cartIconPath from "assets/tabbar/icon-cart.png"; 15 | import cartActiveIconPath from "assets/tabbar/icon-cart-selected.png"; 16 | import userIconPath from "assets/tabbar/icon-user.png"; 17 | import userActiveIconPath from "assets/tabbar/icon-user-selected.png"; 18 | 19 | const tabs = [ 20 | { 21 | text: "首页", 22 | icon: homeIconPath, 23 | iconActive: homeActiveIconPath, 24 | url: "/" 25 | }, 26 | { 27 | text: "分类", 28 | icon: categoryIconPath, 29 | iconActive: categoryActiveIconPath, 30 | url: "/category" 31 | }, 32 | { 33 | text: "星球", 34 | icon: starIconPath, 35 | iconActive: starActiveIconPath, 36 | url: "/star" 37 | }, 38 | { 39 | text: "购物车", 40 | icon: cartIconPath, 41 | iconActive: cartActiveIconPath, 42 | url: "/cart" 43 | }, 44 | { 45 | text: "我的", 46 | icon: userIconPath, 47 | iconActive: userActiveIconPath, 48 | url: "/user" 49 | } 50 | ]; 51 | 52 | const Tabbar = props => { 53 | // 获取tab状态 54 | const selectedTab = useSelector(state => state.home.selectedTab); 55 | const dispatch = useDispatch(); 56 | 57 | return ( 58 | <> 59 |
      60 | {tabs.map((tab, index) => ( 61 | 62 | {tab.text} 67 | dispatch(actionCreators.selectTabActionCreator(tab.url)) 68 | } 69 | /> 70 | 75 | {tab.text} 76 | 77 | 78 | ))} 79 |
    80 | 81 | ); 82 | }; 83 | 84 | export default Tabbar; 85 | -------------------------------------------------------------------------------- /weapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weapp", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "仿小米商城微信小程序前端", 6 | "templateInfo": { 7 | "name": "redux", 8 | "typescript": false, 9 | "css": "sass" 10 | }, 11 | "scripts": { 12 | "build:weapp": "taro build --type weapp", 13 | "build:swan": "taro build --type swan", 14 | "build:alipay": "taro build --type alipay", 15 | "build:tt": "taro build --type tt", 16 | "build:h5": "taro build --type h5", 17 | "build:rn": "taro build --type rn", 18 | "build:qq": "taro build --type qq", 19 | "build:quickapp": "taro build --type quickapp", 20 | "dev:weapp": "npm run build:weapp -- --watch", 21 | "dev:swan": "npm run build:swan -- --watch", 22 | "dev:alipay": "npm run build:alipay -- --watch", 23 | "dev:tt": "npm run build:tt -- --watch", 24 | "dev:h5": "npm run build:h5 -- --watch", 25 | "dev:rn": "npm run build:rn -- --watch", 26 | "dev:qq": "npm run build:qq -- --watch", 27 | "dev:quickapp": "npm run build:quickapp -- --watch" 28 | }, 29 | "author": "", 30 | "license": "MIT", 31 | "dependencies": { 32 | "@tarojs/components": "2.2.3", 33 | "@tarojs/components-qa": "2.2.3", 34 | "@tarojs/redux": "2.2.3", 35 | "@tarojs/redux-h5": "2.2.3", 36 | "@tarojs/router": "2.2.3", 37 | "@tarojs/taro": "2.2.3", 38 | "@tarojs/taro-alipay": "2.2.3", 39 | "@tarojs/taro-h5": "2.2.3", 40 | "@tarojs/taro-qq": "2.2.3", 41 | "@tarojs/taro-quickapp": "2.2.3", 42 | "@tarojs/taro-swan": "2.2.3", 43 | "@tarojs/taro-tt": "2.2.3", 44 | "@tarojs/taro-weapp": "2.2.3", 45 | "babel-runtime": "^6.26.0", 46 | "nervjs": "^1.5.5", 47 | "nerv-devtools": "^1.5.5", 48 | "redux": "^4.0.0", 49 | "redux-logger": "^3.0.6", 50 | "redux-thunk": "^2.3.0", 51 | "regenerator-runtime": "0.11.1" 52 | }, 53 | "devDependencies": { 54 | "@types/react": "^16.4.8", 55 | "@types/webpack-env": "^1.13.6", 56 | "@tarojs/mini-runner": "2.2.3", 57 | "@tarojs/webpack-runner": "2.2.3", 58 | "babel-plugin-transform-class-properties": "^6.24.1", 59 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 60 | "babel-plugin-transform-jsx-stylesheet": "^0.6.5", 61 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 62 | "babel-plugin-transform-runtime": "^6.23.0", 63 | "babel-preset-env": "^1.6.1", 64 | "babel-eslint": "^8.2.3", 65 | "eslint": "^5.16.0", 66 | "eslint-config-taro": "2.2.3", 67 | "eslint-plugin-react": "^7.8.2", 68 | "eslint-plugin-react-hooks": "^1.6.1", 69 | "eslint-plugin-import": "^2.12.0", 70 | "stylelint": "9.3.0", 71 | "stylelint-config-taro-rn": "2.2.3", 72 | "stylelint-taro-rn": "2.2.3", 73 | "eslint-plugin-taro": "2.2.3" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /h5/src/pages/Category/LeftPanel/LeftPanel.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import * as actionCreators from "store/actionCreators/index"; 4 | import * as styles from "./leftPanel.module.scss"; 5 | 6 | const cates = [ 7 | { 8 | key: "01", 9 | text: "新品", 10 | anchor: "新品" 11 | }, 12 | { 13 | key: "02", 14 | text: "众筹", 15 | anchor: "众筹" 16 | }, 17 | { 18 | key: "03", 19 | text: "小米手机" 20 | }, 21 | { 22 | key: "04", 23 | text: "Redmi" 24 | }, 25 | { 26 | key: "05", 27 | text: "黑鲨" 28 | }, 29 | { 30 | key: "06", 31 | text: "5G合约" 32 | }, 33 | { 34 | key: "07", 35 | text: "手机配件" 36 | }, 37 | { 38 | key: "08", 39 | text: "电视" 40 | }, 41 | { 42 | key: "09", 43 | text: "电视" 44 | }, 45 | { 46 | key: "10", 47 | text: "大家电" 48 | }, 49 | { 50 | key: "11", 51 | text: "电脑办公" 52 | }, 53 | { 54 | key: "12", 55 | text: "小爱智能" 56 | }, 57 | { 58 | key: "13", 59 | text: "路由器" 60 | }, 61 | { 62 | key: "14", 63 | text: "生活电器" 64 | }, 65 | { 66 | key: "15", 67 | text: "厨房电器" 68 | }, 69 | { 70 | key: "16", 71 | text: "智能穿戴" 72 | }, 73 | { 74 | key: "17", 75 | text: "智能家居" 76 | }, 77 | { 78 | key: "18", 79 | text: "车载出行" 80 | }, 81 | { 82 | key: "19", 83 | text: "个护健康" 84 | }, 85 | { 86 | key: "20", 87 | text: "数码配件" 88 | }, 89 | { 90 | key: "21", 91 | text: "日用百货" 92 | }, 93 | { 94 | key: "22", 95 | text: "服务" 96 | }, 97 | { 98 | key: "23", 99 | text: "米粉卡" 100 | }, 101 | { 102 | key: "24", 103 | text: "全部商品" 104 | }, 105 | { 106 | key: "25", 107 | text: "零售店" 108 | } 109 | ]; 110 | 111 | export default function LeftPanel(props) { 112 | const selector = useSelector(state => state.category.setCate); 113 | const dispatch = useDispatch(); 114 | 115 | const setCateInStore = (cate, anchor) => { 116 | const action = actionCreators.setCate(cate); 117 | dispatch(action); 118 | const { goScroll } = props; 119 | goScroll(anchor); 120 | }; 121 | 122 | return ( 123 |
    124 |
      125 | {cates.map(cate => ( 126 |
    • setCateInStore(cate.key, cate.anchor)} 135 | > 136 | {cate.text} 137 |
    • 138 | ))} 139 |
    140 |
    141 | ); 142 | } 143 | -------------------------------------------------------------------------------- /h5/src/utils/request/index.js: -------------------------------------------------------------------------------- 1 | /** 主要看下涉及哪些点 2 | * 01.taro从0到1项目架构课程介绍 3 | 02.初始化项目流程介绍、目录设计 4 | 03.让alias别名解决路径引用的烦恼 5 | 04.请求api返回redux的状态流程 6 | 05.封装request get请求,给url添加时间戳防止浏览器缓存 7 | 06.封装request post Content-Type 分类请求 8 | 07.把taro-advance脚手架推送到私有仓库 9 | 08.弱网请求失败时自动发起api重试 10 | 09.异常日志上报封装设计思路 11 | 10.异常日志上报封装,五种级别输出。 12 | 11.上报收集日志平台系统介绍 13 | 12.实战接入日志平台 14 | 13.深度序列化错误error控制台上报 15 | 14.登录流程讲解(前端和后端实现流程) 16 | 15.登录实现详细讲解(token附加到请求header头) 17 | 16.用户授权后更新用户信息流程 18 | 17.设计createApiAction自动dispatch优化开发体验 19 | 18.改造actionType支持庞大业务 20 | 19.Action三种ActionType的集合 21 | 20.简化reducers的swich繁琐操作 22 | 21.增加request的状态 23 | 22.课程总结 24 | */ 25 | 26 | import { REQUEST_METHOD, BASE_URL } from "./config"; 27 | 28 | /** 29 | * 封装网路请求,基于fetch 30 | * 注入token 31 | */ 32 | export class Request { 33 | /** 34 | * 发起请求 35 | * @param {*} url 36 | * @param {*} config 37 | */ 38 | async fetchData(url, config) { 39 | let result; 40 | const Host = 41 | process.env.NODE_ENV === "development" 42 | ? process.env.REACT_APP_DEV_HOST 43 | : (process.env.REACT_APP_PROD_HOST = "huiduoduo.online"); 44 | const requestUrl = `${Host}${BASE_URL}${url}`; 45 | const { method, contentType, token, body } = config; 46 | // 创建headers 47 | const headers = new Headers({ 48 | token: !token ? sessionStorage.getItem("MI_SHOP_TOKEN") : token, 49 | "Content-Type": contentType 50 | }); 51 | // 根据请求方法判断 52 | if (!method || method === REQUEST_METHOD.GET) { 53 | result = await fetch(requestUrl, { 54 | headers 55 | }); 56 | } else if (method === REQUEST_METHOD.POST) { 57 | result = await fetch(requestUrl, { 58 | body: JSON.stringify(body), 59 | headers, 60 | method 61 | }); 62 | } else { 63 | result = await fetch(requestUrl, { 64 | body: JSON.stringify(body), 65 | headers, 66 | method 67 | }); 68 | } 69 | return this.handleRequest(result); 70 | } 71 | 72 | /** 73 | * 处理相应 74 | * @param {*} result 75 | */ 76 | async handleRequest(result) { 77 | const parsedRequest = await this.parseRequest(result); 78 | // 如果res.ok,则请求成功 79 | if (result.ok) { 80 | return parsedRequest; 81 | } 82 | // 请求失败,返回解析之后的失败的数据 83 | const error = parsedRequest; 84 | throw error; 85 | } 86 | 87 | /** 88 | * 根据Content-Type解析返回内容 89 | * @param {*} result 90 | */ 91 | async parseRequest(result) { 92 | const contentType = result.headers.get("Content-Type"); 93 | if (!contentType) return await result.text(); 94 | if (contentType.indexOf("json") > -1) { 95 | return await result.json(); 96 | } 97 | if (contentType.indexOf("text") > -1) { 98 | return await result.text(); 99 | } 100 | if (contentType.indexOf("form") > -1) { 101 | return await result.formData(); 102 | } 103 | if (contentType.indexOf("video") > -1) { 104 | return await result.blob(); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /h5/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
    10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
    13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
    18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
    23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
    26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `yarn build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /h5/src/pages/Home/Navbar/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import * as actionCreator from "store/actionCreators/index"; 4 | import * as styles from "./navbar.module.scss"; 5 | import arrowUpIcon from "assets/home/arrow-down.png"; 6 | import arrowDownIcon from "assets/home/arrow-up.png"; 7 | 8 | const navItems = [ 9 | { 10 | text: "推荐", 11 | key: "recommend" 12 | }, 13 | { 14 | text: "手机", 15 | key: "cellphone" 16 | }, 17 | { 18 | text: "智能", 19 | key: "smartDevice" 20 | }, 21 | { 22 | text: "电视", 23 | key: "tv" 24 | }, 25 | { 26 | text: "笔记本", 27 | key: "laptop" 28 | }, 29 | { 30 | text: "家电", 31 | key: "appliance" 32 | }, 33 | { 34 | text: "生活周边", 35 | key: "others" 36 | } 37 | ]; 38 | 39 | export default function Navbar() { 40 | // 获取nav状态 41 | const selector = useSelector(state => state.home.nav); 42 | const dispatch = useDispatch(); 43 | 44 | const [collapse, setCollapse] = useState(false); 45 | 46 | return ( 47 |
    48 |
    49 | {getSlicedArray(navItems).map((item, index) => ( 50 |
    54 | dispatch(actionCreator.selectNavActionCreator(item.key)) 55 | } 56 | > 57 | 64 | {item.text} 65 | 66 |
    67 | ))} 68 |
    setCollapse(true)} 71 | > 72 | 折叠 73 |
    74 |
    75 |
    76 |
    全部
    77 | 收起 setCollapse(false)} 82 | /> 83 |
    84 | {navItems.map((nav, index) => ( 85 | 95 | dispatch(actionCreator.selectNavActionCreator(nav.key)) 96 | } 97 | > 98 | {nav.text} 99 | 100 | ))} 101 |
    102 |
    103 |
    104 | ); 105 | } 106 | 107 | const getSlicedArray = array => { 108 | const newArr = array.length > 6 ? array.slice(0, 6) : array; 109 | return newArr; 110 | }; 111 | -------------------------------------------------------------------------------- /h5/src/pages/Category/RightPanel/RightPanel.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as styles from "./rightPanel.module.scss"; 3 | 4 | import banner01 from "assets/category/banner01.jpg"; 5 | import pic01 from "assets/category/小米10 青春.png"; 6 | import pic02 from "assets/category/K30 Pro 变焦版.png"; 7 | import pic03 from "assets/category/K30 Pro.png"; 8 | 9 | const SectionComponent = (props) => { 10 | const { sections } = props; 11 | 12 | return ( 13 | <> 14 | {sections.map((section) => ( 15 |
    16 | {section.bannerPath ? ( 17 | icon 22 | ) : null} 23 |
    24 | {section.title} 25 |
    26 |
    27 | {section.items.map((item) => ( 28 |
    29 | icon 34 | {item.text} 35 |
    36 | ))} 37 |
    38 |
    39 | ))} 40 | 41 | ); 42 | }; 43 | 44 | export default function RightPanel() { 45 | const sections = [ 46 | { 47 | bannerPath: banner01, 48 | key: "section01", 49 | title: "手机", 50 | anchor: "新品", 51 | items: [ 52 | { 53 | key: "section01-item01", 54 | imgUrl: pic01, 55 | text: "小米10 青春", 56 | }, 57 | { 58 | key: "section01-item02", 59 | imgUrl: pic02, 60 | text: "K30 Pro 变焦版", 61 | }, 62 | { 63 | key: "section01-item03", 64 | imgUrl: pic03, 65 | text: "K30 Pro", 66 | }, 67 | { 68 | key: "section01-item04", 69 | imgUrl: pic01, 70 | text: "小米10 青春", 71 | }, 72 | { 73 | key: "section01-item05", 74 | imgUrl: pic02, 75 | text: "K30 Pro 变焦版", 76 | }, 77 | { 78 | key: "section01-item06", 79 | imgUrl: pic03, 80 | text: "K30 Pro", 81 | }, 82 | { 83 | key: "section01-item07", 84 | imgUrl: pic01, 85 | text: "小米10 青春", 86 | }, 87 | ], 88 | }, 89 | { 90 | bannerPath: null, 91 | key: "section02", 92 | title: "电脑", 93 | anchor: "众筹", 94 | items: [ 95 | { 96 | key: "section02-item01", 97 | imgUrl: pic01, 98 | text: "小米10 青春", 99 | }, 100 | { 101 | key: "section02-item02", 102 | imgUrl: pic02, 103 | text: "K30 Pro 变焦版", 104 | }, 105 | { 106 | key: "section02-item03", 107 | imgUrl: pic03, 108 | text: "K30 Pro", 109 | }, 110 | { 111 | key: "section02-item04", 112 | imgUrl: pic01, 113 | text: "小米10 青春", 114 | }, 115 | { 116 | key: "section02-item05", 117 | imgUrl: pic02, 118 | text: "K30 Pro 变焦版", 119 | }, 120 | { 121 | key: "section02-item06", 122 | imgUrl: pic03, 123 | text: "K30 Pro", 124 | }, 125 | { 126 | key: "section02-item07", 127 | imgUrl: pic01, 128 | text: "小米10 青春", 129 | }, 130 | ], 131 | }, 132 | ]; 133 | 134 | return ( 135 |
    136 | 137 |
    138 | ); 139 | } 140 | -------------------------------------------------------------------------------- /server/src/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Service, Inject } from 'typedi'; 2 | import jwt from 'jsonwebtoken'; 3 | import crypto from 'crypto'; 4 | import config from '../config'; 5 | import logger from '../loaders/logger'; 6 | import userModel from '../models/user.model'; 7 | import { IUser, UserDTO } from '../interfaces/IUser'; 8 | 9 | @Service() 10 | export default class AuthSerice { 11 | // random salt 12 | private getRandomSalt() { 13 | return Math.random().toString().slice(2, 5); 14 | } 15 | 16 | // encrypt pwd via md5 with salt 17 | private encryptPwd(pwd: string, salt: string) { 18 | const saltedPwd = `${pwd}:${salt}`; 19 | 20 | // 加盐密码的md5值 21 | const md5 = crypto.createHash('md5'); 22 | const result = md5.update(saltedPwd).digest('hex'); 23 | return result; 24 | } 25 | 26 | // generate jwt 27 | private generateJwt(user: any) { 28 | // 设置0.5小时过期 29 | const today = new Date(); 30 | const exp = new Date(today); 31 | exp.setHours(today.getHours() + 0.5); 32 | logger.silly(`Sign JWT for userId: ${user._id}`); 33 | const jwtSecret = config.jwtSecret; 34 | return jwt.sign( 35 | { 36 | _id: user._id, // We are gonna use this in the middleware 'isAuth' 37 | role: user.role, 38 | name: user.name, 39 | exp: exp.getTime() / 1000, 40 | }, 41 | jwtSecret, 42 | ); 43 | } 44 | 45 | /** 46 | * 注册 47 | * @param userDTO 48 | */ 49 | public async signUp(userDTO: UserDTO): Promise<{ user: IUser; token: string; message: string }> { 50 | try { 51 | logger.silly('Hashing password'); 52 | // 生成随机salt 53 | const salt = this.getRandomSalt(); 54 | // 密码加密 55 | const hashedPwd = this.encryptPwd(userDTO.password, salt); 56 | logger.silly('Creating user db record'); 57 | 58 | const userRecord = await new userModel({ 59 | ...userDTO, 60 | salt, 61 | password: hashedPwd, 62 | }); 63 | 64 | // 保存到数据库 65 | await userRecord.save(); 66 | 67 | logger.silly('Generating JWT'); 68 | const token = this.generateJwt(userRecord); 69 | 70 | const user = userRecord.toObject(); 71 | Reflect.deleteProperty(user, 'password'); 72 | Reflect.deleteProperty(user, 'salt'); 73 | return { user, token, message: '用户注册成功!' }; 74 | } catch (error) { 75 | logger.error(error); 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * 登录 82 | * @param email 83 | * @param pwd 84 | */ 85 | public async signIn(email: string, pwd: string): Promise<{ user: IUser; token: string; message: string }> { 86 | const userRecord = await userModel.findOne({ email }); 87 | if (!userRecord) { 88 | logger.info('User not registered'); 89 | const user = { email, password: pwd }; 90 | return await this.signUp(user); 91 | } 92 | logger.silly('Checking password'); 93 | const { password, salt } = userRecord; 94 | // 通过用户登录密码和存起来的salt,获得hash2 95 | const hash2 = this.encryptPwd(pwd, salt); 96 | // 比较password和hash2即可 97 | if (password === hash2) { 98 | logger.silly('Password is valid!'); 99 | logger.silly('Generating JWT'); 100 | 101 | const token = this.generateJwt(userRecord); 102 | 103 | const user = userRecord.toObject(); 104 | Reflect.deleteProperty(user, 'password'); 105 | Reflect.deleteProperty(user, 'salt'); 106 | return { user, token, message: '用户登录成功!' }; 107 | } else { 108 | throw new Error('Invalid Password'); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /h5/src/pages/Login/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | import { useSelector, useDispatch } from "react-redux"; 4 | import * as actionCreators from "store/actionCreators"; 5 | import { useForm } from "react-hook-form"; 6 | import { Request } from "utils/request/index"; 7 | import { REQUEST_METHOD, CONTENT_TYPE } from "utils/request/config"; 8 | import * as styles from "./login.module.scss"; 9 | import miIcon from "assets/login/mi.png"; 10 | import eyeIcon from "assets/login/eye.svg"; 11 | import eyeHideIcon from "assets/login/eye-hide.svg"; 12 | 13 | export default function Login(props) { 14 | // 本地状态管理 15 | const [isPasswordHidden, setHide] = useState(); 16 | const [account, setAccount] = useState(""); 17 | const [pwd, setPwd] = useState(""); 18 | 19 | // 全家状态管理 20 | let token = useSelector(state => state.auth.token); 21 | const dispatch = useDispatch(); 22 | 23 | // 表单验证 24 | const { register, handleSubmit, errors } = useForm(); 25 | 26 | // 路由跳转 27 | const history = useHistory(); 28 | 29 | // 回到上一页 30 | const goBack = () => { 31 | history.goBack(); 32 | }; 33 | 34 | // 切换显示 35 | const shiftPwdVisibility = () => { 36 | // 切换状态 37 | let reverse = !isPasswordHidden; 38 | setHide(reverse); 39 | const inputEl = document.querySelector("input#password"); 40 | // 通过切换type值,实现隐藏pwd 41 | if (isPasswordHidden) { 42 | inputEl.setAttribute("type", "password"); 43 | } else { 44 | inputEl.setAttribute("type", "text"); 45 | } 46 | }; 47 | 48 | /** 49 | * 提交表单 50 | * @param {} e 51 | */ 52 | const handleClick = async () => { 53 | let request = new Request(); 54 | try { 55 | let response = await request.fetchData("/auth/verify", { 56 | method: REQUEST_METHOD.POST, 57 | contentType: CONTENT_TYPE.JSON, 58 | token, 59 | body: { 60 | email: account, 61 | password: pwd 62 | } 63 | }); 64 | dispatch(actionCreators.login(response.token)); 65 | goBack(); 66 | console.log(response); 67 | } catch (error) { 68 | console.log(error); 69 | } 70 | }; 71 | 72 | return ( 73 |
    74 |
    75 | 小米 76 |

    小米帐号登录/注册

    77 |
    78 |
    79 |
    83 | setAccount(e.currentTarget.value)} 88 | placeholder="邮箱" 89 | ref={register({ required: true, pattern: /\S+@\S+\.\S+/ })} 90 | /> 91 | {errors.email?.type === "required" && ( 92 | 请输入邮箱 93 | )} 94 | {errors.email?.type === "pattern" && ( 95 | 96 | 邮箱格式不正确 97 | 98 | )} 99 | setPwd(e.currentTarget.value)} 104 | placeholder="密码" 105 | ref={register({ 106 | required: true, 107 | pattern: /^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{6,16}$/ 108 | })} 109 | /> 110 | {errors.password?.type === "required" && ( 111 | 请输入密码 112 | )} 113 | {errors.password?.type === "pattern" && ( 114 | 115 | 密码格式为6-16位,必须包含数字,特殊字符,大小写字母 116 | 117 | )} 118 | 123 | 124 | 130 |
    131 |
    132 |
    133 | ); 134 | } 135 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": true /* Generates corresponding '.map' file. */, 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./dist" /* Redirect output structure to the directory. */, 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./" /* Base directory to resolve non-absolute module names. */, 44 | // "paths": {} /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */, 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 61 | "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */ 62 | }, 63 | // "include": ["src"], 64 | "exclude": ["node_modules"] 65 | } 66 | --------------------------------------------------------------------------------