├── src ├── pages │ ├── index.less │ ├── common-components │ │ ├── no-data │ │ │ ├── index.jsx │ │ │ └── index.less │ │ ├── badge │ │ │ ├── index.jsx │ │ │ └── index.less │ │ ├── lazy-loading │ │ │ ├── index.jsx │ │ │ └── index.less │ │ ├── title-bar │ │ │ ├── index.jsx │ │ │ └── index.less │ │ ├── gallery │ │ │ ├── index.less │ │ │ └── index.jsx │ │ ├── address-row │ │ │ ├── index.jsx │ │ │ └── index.less │ │ ├── skeleton │ │ │ ├── recommend.jsx │ │ │ ├── entry.jsx │ │ │ ├── row.jsx │ │ │ └── index.less │ │ ├── auth-err │ │ │ ├── index.jsx │ │ │ └── index.less │ │ ├── nav-bar │ │ │ ├── index.less │ │ │ └── index.jsx │ │ ├── recommend-food-row │ │ │ ├── index.jsx │ │ │ └── index.less │ │ ├── tab-bar │ │ │ ├── index.less │ │ │ └── index.jsx │ │ ├── red-ticket-row │ │ │ ├── index.jsx │ │ │ └── index.less │ │ └── order-list-row │ │ │ ├── index.jsx │ │ │ └── index.less │ ├── compass │ │ ├── index.less │ │ └── index.jsx │ ├── benefit │ │ ├── index.less │ │ └── index.jsx │ ├── order │ │ ├── index.less │ │ └── index.jsx │ ├── place-order │ │ ├── index.less │ │ └── index.jsx │ ├── home │ │ ├── skeleton-screen │ │ │ └── index.jsx │ │ ├── index.less │ │ └── top-bar │ │ │ ├── index.jsx │ │ │ └── index.less │ ├── address │ │ ├── address-row │ │ │ ├── index.jsx │ │ │ └── index.less │ │ ├── index.less │ │ └── index.jsx │ ├── shop-detail │ │ ├── foods │ │ │ └── menu.jsx │ │ ├── skeleton-screen │ │ │ ├── index.jsx │ │ │ └── index.less │ │ ├── shop │ │ │ ├── index.less │ │ │ └── index.jsx │ │ └── cartcontrol │ │ │ └── stepper.jsx │ ├── search-shop │ │ ├── list.jsx │ │ ├── index.less │ │ └── index.jsx │ ├── address-nearby │ │ └── index.less │ ├── index.js │ ├── address-edit │ │ └── index.less │ ├── restaurant │ │ └── index.jsx │ ├── order-detail │ │ └── index.less │ ├── login │ │ └── index.less │ └── profile │ │ ├── index.less │ │ └── index.jsx ├── assets │ ├── img │ │ └── thanks.gif │ ├── svg │ │ ├── compass.svg │ │ ├── triangle_down_fill.svg │ │ ├── back.svg │ │ ├── unfold.svg │ │ ├── people.svg │ │ ├── right.svg │ │ ├── close.svg │ │ ├── filter.svg │ │ ├── check.svg │ │ ├── form.svg │ │ ├── iphone.svg │ │ ├── crown.svg │ │ ├── location.svg │ │ ├── search.svg │ │ ├── edit.svg │ │ ├── delete.svg │ │ ├── round_add.svg │ │ ├── success.svg │ │ ├── refresh.svg │ │ ├── avatar.svg │ │ ├── elem.svg │ │ ├── cry.svg │ │ ├── cart.svg │ │ ├── new.svg │ │ ├── drumstick.svg │ │ ├── shop.svg │ │ └── gold.svg │ └── css │ │ ├── common.less │ │ ├── theme.less │ │ └── mixin.less ├── components │ ├── loading │ │ ├── loading.gif │ │ ├── index.less │ │ └── index.jsx │ ├── icon-svg │ │ ├── index.less │ │ └── index.jsx │ ├── rate │ │ ├── index.less │ │ └── index.jsx │ ├── vertical-slide │ │ ├── index.less │ │ └── index.jsx │ ├── async-loade.jsx │ ├── modal │ │ └── index.jsx │ ├── scroll │ │ └── index.less │ ├── slide │ │ └── index.less │ └── toast │ │ ├── index.less │ │ ├── index.jsx │ │ ├── notices.jsx │ │ └── notification.jsx ├── utils │ ├── config.js │ ├── event-proxy.js │ ├── utils.js │ └── dom.js ├── stores │ ├── global.js │ ├── shopping-cart.js │ ├── index.js │ ├── order.js │ ├── compass.js │ ├── shop.js │ ├── search-shop.js │ └── home.js ├── index.js ├── index.html ├── api │ ├── http.js │ └── index.js └── .eslintrc ├── screenshot ├── demo.gif ├── find.png ├── home.png ├── login.png ├── order.png ├── user.png ├── hongbao.png ├── search.png ├── my-address.png ├── shop-list.png ├── order-detail.png ├── shop-detail.png ├── shop-list-1.png ├── shop-list-2.png ├── search-address.png └── update-address.png ├── .gitignore ├── .editorconfig ├── .postcssrc.js ├── .babelrc ├── LICENSE ├── static └── css │ └── reset.css ├── README.md └── package.json /src/pages/index.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screenshot/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/demo.gif -------------------------------------------------------------------------------- /screenshot/find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/find.png -------------------------------------------------------------------------------- /screenshot/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/home.png -------------------------------------------------------------------------------- /screenshot/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/login.png -------------------------------------------------------------------------------- /screenshot/order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/order.png -------------------------------------------------------------------------------- /screenshot/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/user.png -------------------------------------------------------------------------------- /screenshot/hongbao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/hongbao.png -------------------------------------------------------------------------------- /screenshot/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/search.png -------------------------------------------------------------------------------- /screenshot/my-address.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/my-address.png -------------------------------------------------------------------------------- /screenshot/shop-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/shop-list.png -------------------------------------------------------------------------------- /src/assets/img/thanks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/src/assets/img/thanks.gif -------------------------------------------------------------------------------- /screenshot/order-detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/order-detail.png -------------------------------------------------------------------------------- /screenshot/shop-detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/shop-detail.png -------------------------------------------------------------------------------- /screenshot/shop-list-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/shop-list-1.png -------------------------------------------------------------------------------- /screenshot/shop-list-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/shop-list-2.png -------------------------------------------------------------------------------- /screenshot/search-address.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/search-address.png -------------------------------------------------------------------------------- /screenshot/update-address.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/screenshot/update-address.png -------------------------------------------------------------------------------- /src/components/loading/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaojingran/react-eleme/HEAD/src/components/loading/loading.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | node_modules/ 4 | npm-debug.log 5 | dist/ 6 | .idea 7 | .vscode 8 | tree.md 9 | *.idea 10 | -------------------------------------------------------------------------------- /src/components/icon-svg/index.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | .icon-svg { 4 | width: 1em; 5 | height: 1em; 6 | vertical-align: middle; 7 | fill: currentColor; 8 | overflow: hidden; 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [*] 5 | chartset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /src/pages/common-components/no-data/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import styles from './index.less' 5 | 6 | export default ({ text = '没有更多数据' }) => ( 7 |

{text}

8 | ) 9 | -------------------------------------------------------------------------------- /src/pages/common-components/no-data/index.less: -------------------------------------------------------------------------------- 1 | 2 | @import '../../../assets/css/theme.less'; 3 | 4 | .desc { 5 | text-align: center; 6 | font-size: @font-size-medium; 7 | font-weight: @font-weight-small; 8 | color: @color-base; 9 | padding: 20px 0; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/rate/index.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | .rate-wrapper { 4 | position: relative; 5 | width: 100%; 6 | height: 100%; 7 | .rate { 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | bottom: 0; 12 | width: 0; 13 | overflow: hidden; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/pages/common-components/badge/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import cls from 'classnames' 4 | import styles from './index.less' 5 | 6 | export default ({ text, className, style }) => ( 7 |
8 | ) 9 | -------------------------------------------------------------------------------- /src/pages/common-components/lazy-loading/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import science from 'assets/img/science.svg' 4 | import styles from './index.less' 5 | 6 | export default () => ( 7 |
8 |
9 | 10 |
11 |
12 | ) 13 | -------------------------------------------------------------------------------- /src/pages/common-components/title-bar/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import styles from './index.less' 5 | 6 | export default ({ title }) => ( 7 |
8 |
9 |

{title}

10 |
11 |
12 | ) 13 | -------------------------------------------------------------------------------- /src/pages/compass/index.less: -------------------------------------------------------------------------------- 1 | 2 | @import '../../assets/css/theme.less'; 3 | @import '../../assets/css/mixin.less'; 4 | 5 | .compass { 6 | width: 100%; 7 | height: 100%; 8 | background-color: @fill-body; 9 | position: relative; 10 | .scroll { 11 | top: @bar-height; 12 | background-color: @fill-body; 13 | text-align: center; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/loading/index.less: -------------------------------------------------------------------------------- 1 | 2 | @import '../../assets/css/theme.less'; 3 | 4 | .loading { 5 | width: 100%; 6 | text-align: center; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | .desc { 11 | line-height: 20px; 12 | font-size: @font-size-small; 13 | font-weight: @font-weight-small; 14 | color: @color-base; 15 | padding-left: 8px; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | plugins: { 4 | "autoprefixer": {}, 5 | "postcss-px-to-viewport": { 6 | "viewportWidth": 750, 7 | "viewportHeight": 1334, 8 | "unitPrecision": 2, 9 | "viewportUnit": "vw", 10 | "selectorBlackList": [".ignore", ".hairlines"], 11 | "minPixelValue": 1, 12 | "mediaQuery": false 13 | }, 14 | "postcss-viewport-units": {}, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "presets": [ 4 | [ 5 | "env", { 6 | "modules": false, 7 | "targets": { 8 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 9 | }, 10 | "useBuiltIns": true 11 | } 12 | ], 13 | "react", 14 | "stage-0" 15 | ], 16 | "plugins": [ 17 | "react-hot-loader/babel", 18 | "transform-decorators-legacy", 19 | "transform-runtime" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/benefit/index.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | @import '../../assets/css/theme.less'; 4 | @import '../../assets/css/mixin.less'; 5 | 6 | 7 | .benefit { 8 | background-color: @fill-body-darken; 9 | position: fixed; 10 | .position-full; 11 | z-index: 1; 12 | .scroll { 13 | top: @bar-height; 14 | background-color: @fill-body-darken; 15 | box-sizing: border-box; 16 | padding: 0 20px; 17 | padding-top: 20px; 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/pages/common-components/title-bar/index.less: -------------------------------------------------------------------------------- 1 | 2 | @import '../../../assets/css/mixin.less'; 3 | @import '../../../assets/css/theme.less'; 4 | 5 | .title { 6 | width: 100%; 7 | height: 72px; 8 | display: flex; 9 | .flex-center; 10 | .split { 11 | width: 40px; 12 | height: 2px; 13 | background-color: @color-base; 14 | } 15 | .text { 16 | font-size: @font-size-medium-x; 17 | color: @color-title; 18 | margin: 0 26px; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/pages/common-components/lazy-loading/index.less: -------------------------------------------------------------------------------- 1 | 2 | @import '../../../assets/css/theme.less'; 3 | @import '../../../assets/css/mixin.less'; 4 | 5 | .loading { 6 | position: fixed; 7 | .position-full; 8 | background-color: @fill-body-darken; 9 | .icon { 10 | width: 300px; 11 | height: 300px; 12 | position: absolute; 13 | left: 50%; 14 | top: 50%; 15 | transform: translate(-50%, -50%); 16 | img { 17 | width: 100%; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/config.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | shopImgSize: '?imageMogr/format/webp/thumbnail/!130x130r/gravity/Center/crop/130x130/', 4 | sexMap: ['女士', '先生'], 5 | addressTag: ['', '家', '学校', '公司'], 6 | orderByMap: [ 7 | { 8 | id: 0, 9 | name: '综合排序', 10 | }, 11 | { 12 | id: 6, 13 | name: '销量最高', 14 | }, 15 | { 16 | id: 1, 17 | name: '起送价最低', 18 | }, 19 | { 20 | id: 2, 21 | name: '起送价最低', 22 | }, 23 | ], 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/order/index.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | @import '../../assets/css/theme.less'; 4 | @import '../../assets/css/mixin.less'; 5 | 6 | .order { 7 | width: 100%; 8 | height: 100%; 9 | background-color: @fill-body-darken; 10 | position: relative; 11 | z-index: 1; 12 | .nav { 13 | position: absolute; 14 | top: 0; 15 | left: 0; 16 | z-index: 2; 17 | } 18 | .scroll { 19 | overflow: visible; 20 | top: @bar-height; 21 | z-index: 1; 22 | background-color: @fill-body-darken; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/stores/global.js: -------------------------------------------------------------------------------- 1 | 2 | const UPDATE = 'GLOBAL_UPDATE' 3 | 4 | const initState = { 5 | isLogin: false, 6 | userInfo: {}, 7 | } 8 | 9 | export const globalState = (state = initState, action) => { 10 | switch (action.type) { 11 | case UPDATE: 12 | return { 13 | ...state, 14 | ...action.payload, 15 | } 16 | default: 17 | return state 18 | } 19 | } 20 | 21 | export const globalUpdate = (params) => { 22 | return { 23 | payload: params, 24 | type: UPDATE, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/pages/common-components/gallery/index.less: -------------------------------------------------------------------------------- 1 | 2 | .gallery { 3 | position: fixed; 4 | z-index: 1; 5 | top: 0; 6 | left: 0; 7 | right: 0; 8 | bottom: 0; 9 | .slide { 10 | position: absolute; 11 | width: 600px; 12 | top: 50%; 13 | left: 50%; 14 | transform: translate3d(-50%, -50%, 0); 15 | z-index: 2; 16 | } 17 | .mask { 18 | position: fixed; 19 | top: 0; 20 | left: 0; 21 | right: 0; 22 | bottom: 0; 23 | z-index: 1; 24 | background-color: rgba(0,0,0,.5); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/pages/common-components/address-row/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import styles from './index.less' 4 | 5 | export default class AddressRow extends React.PureComponent { 6 | render() { 7 | const { data, handleClick } = this.props 8 | return ( 9 |
10 |

{data.name}

11 |

{data.address}

12 |
13 |
14 | ) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/common-components/badge/index.less: -------------------------------------------------------------------------------- 1 | 2 | @import '../../../assets/css/theme.less'; 3 | 4 | .badge { 5 | background-color: @badge-color-open; 6 | overflow: hidden; 7 | &::after { 8 | position: absolute; 9 | top: 50%; 10 | left: 50%; 11 | content: attr(content) !important; 12 | text-align: center; 13 | color: #fff; 14 | font-size: @font-size-small; 15 | font-weight: @font-weight-small; 16 | transform: scale(.8) translate3D(-50%, -50%, 0); 17 | transform-origin: 0 0; 18 | white-space: nowrap; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/pages/place-order/index.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | @import '../../assets/css/theme.less'; 4 | @import '../../assets/css/mixin.less'; 5 | 6 | .place-order { 7 | position: fixed; 8 | .position-full; 9 | z-index: 2; 10 | background-color: @fill-body; 11 | .content { 12 | text-align: center; 13 | img { 14 | width: 300px; 15 | height: 300px; 16 | content: none !important; 17 | } 18 | p { 19 | text-align: center; 20 | font-weight: @font-weight-small; 21 | color: @color-title; 22 | margin: 20px; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/pages/common-components/skeleton/recommend.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import styles from './index.less' 5 | 6 | export default class RecommendSk extends React.PureComponent { 7 | render() { 8 | return ( 9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/common-components/skeleton/entry.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import styles from './index.less' 5 | 6 | export default class EntrySk extends React.PureComponent { 7 | render() { 8 | return ( 9 |
10 | { 11 | Array.from({ length: 10 }, (v, i) => i).map(v => ( 12 |
13 |
14 |
15 |
16 | )) 17 | } 18 |
19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/common-components/skeleton/row.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import styles from './index.less' 4 | 5 | export default class RowSK extends React.PureComponent { 6 | render() { 7 | return ( 8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/icon-svg/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import cls from 'classnames' 4 | import PropTypes from 'prop-types' 5 | import styles from './index.less' 6 | 7 | const SvgIcon = ({ name, className, onClick = () => {} }) => { 8 | const clsName = className ? cls(styles['icon-svg'], className) : styles['icon-svg']; 9 | return ( 10 | 13 | ); 14 | }; 15 | 16 | SvgIcon.propTypes = { 17 | name: PropTypes.string, 18 | className: PropTypes.string, 19 | }; 20 | 21 | export default SvgIcon 22 | -------------------------------------------------------------------------------- /src/pages/home/skeleton-screen/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import EntrySk from '../../common-components/skeleton/entry' 5 | import RowSk from '../../common-components/skeleton/row' 6 | 7 | export default class SkeletionScreen extends React.PureComponent { 8 | render() { 9 | return ( 10 |
11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/vertical-slide/index.less: -------------------------------------------------------------------------------- 1 | 2 | .slide { 3 | min-height: 1px; 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | right: 0; 8 | bottom: 0; 9 | overflow: hidden; 10 | .slide-group { 11 | position: relative; 12 | overflow: hidden; 13 | white-space: nowrap; 14 | .slide-item { 15 | box-sizing: border-box; 16 | overflow: hidden; 17 | a { 18 | display: block; 19 | height: 100%; 20 | overflow: hidden; 21 | text-decoration: none; 22 | } 23 | img { 24 | display: block; 25 | height: 100%; 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/assets/svg/compass.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/place-order/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import NavBar from '../common-components/nav-bar' 5 | import thanks from '../../assets/img/thanks.gif' 6 | import styles from './index.less' 7 | 8 | export default class PlaceOrder extends React.Component { 9 | render() { 10 | return ( 11 |
12 | this.props.history.goBack()} /> 16 |
17 | 18 |

老铁!给个star吧!

19 |
20 |
21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/assets/svg/triangle_down_fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/back.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/stores/shopping-cart.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const UPDATE = 'SHOPPINGCART_UPDATE' 4 | 5 | // { 6 | // restaurant_id: '商店id', 7 | // food_id: '商品id', 8 | // name: '名字', 9 | // price: '价格', 10 | // quantity: '数量', 11 | // attrs: '属性', 12 | // new_specs: '规格' 13 | // } 14 | 15 | const initState = { 16 | cart: [], 17 | } 18 | 19 | export const shoppingCart = (state = initState, action) => { 20 | switch (action.type) { 21 | case UPDATE: 22 | return { 23 | ...state, 24 | ...action.payload, 25 | } 26 | default: 27 | return state 28 | } 29 | } 30 | 31 | export const shoppingCartUpdate = (params) => { 32 | return { 33 | payload: params, 34 | type: UPDATE, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/pages/common-components/address-row/index.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | @import '../../../assets/css/theme.less'; 4 | @import '../../../assets/css/mixin.less'; 5 | 6 | .row { 7 | .title { 8 | padding-right: 30px; 9 | box-sizing: border-box; 10 | font-size: @font-size-medium; 11 | color: @color-title; 12 | line-height: 32px; 13 | .no-wrap; 14 | padding-top: 20px; 15 | } 16 | .desc { 17 | padding-right: 30px; 18 | box-sizing: border-box; 19 | font-size: @font-size-small; 20 | font-weight: @font-weight-small; 21 | color: @color-base; 22 | line-height: 32px; 23 | .no-wrap; 24 | margin: 14px 0 20px 0; 25 | } 26 | .line { 27 | width: 100%; 28 | height: 1px; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/pages/common-components/auth-err/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import { withRouter } from 'react-router-dom' 5 | import styles from './index.less' 6 | 7 | @withRouter 8 | export default class AuthErr extends React.PureComponent { 9 | render() { 10 | const goLogin = () => { 11 | this.props.history.push('/login') 12 | } 13 | return ( 14 |
15 |
16 | 17 |
18 |

登录后查看更多信息

19 | 20 |
21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/async-loade.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | export default (loadCompoent, loading) => { 5 | return class AsyncComponet extends React.Component { 6 | constructor(props) { 7 | super(props) 8 | this.state = { 9 | C: null, 10 | } 11 | this.unmount = false 12 | } 13 | 14 | async componentDidMount() { 15 | const { default: C } = await loadCompoent() 16 | if (this.unmount) return 17 | this.setState({ C }) // eslint-disable-line 18 | } 19 | 20 | componentWillUnmount() { 21 | this.unmount = true 22 | } 23 | 24 | render() { 25 | const { C } = this.state; 26 | return C ? : loading 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/assets/css/common.less: -------------------------------------------------------------------------------- 1 | 2 | @import './theme.less'; 3 | 4 | :global { 5 | .hairline-h { 6 | position: relative; 7 | border: none; 8 | &:after { 9 | content: '' !important; 10 | position: absolute; 11 | left: 0; 12 | background: @hairline-color; 13 | width: 100%; 14 | height: 1px; 15 | transform: scaleY(0.5); 16 | transform-origin: 0 0; 17 | } 18 | } 19 | 20 | .hairline-v { 21 | position: relative; 22 | border: none; 23 | &:after { 24 | content: '' !important; 25 | position: absolute; 26 | left: 0; 27 | background: @hairline-color; 28 | height: 100%; 29 | width: 1px; 30 | transform: scaleX(0.5); 31 | transform-origin: 0 0; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/stores/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | import { 4 | createStore, 5 | applyMiddleware, 6 | combineReducers, 7 | } from 'redux' 8 | import thunk from 'redux-thunk' 9 | import { home } from './home' 10 | import { globalState } from './global' 11 | import { order } from './order' 12 | import { shop } from './shop' 13 | import { compass } from './compass' 14 | import { restaurant } from './restaurant' 15 | import { searchShop } from './search-shop' 16 | import { shoppingCart } from './shopping-cart' 17 | 18 | const store = createStore( 19 | combineReducers({ 20 | home, 21 | globalState, 22 | order, 23 | shop, 24 | compass, 25 | restaurant, 26 | searchShop, 27 | shoppingCart, 28 | }), 29 | applyMiddleware(thunk), 30 | ) 31 | 32 | export default store 33 | -------------------------------------------------------------------------------- /src/assets/svg/unfold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/people.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/common-components/auth-err/index.less: -------------------------------------------------------------------------------- 1 | 2 | @import '../../../assets/css/theme.less'; 3 | 4 | .err { 5 | width: 100%; 6 | text-align: center; 7 | .img { 8 | width: 600px; 9 | margin: 0 auto; 10 | img { 11 | width: 100%; 12 | } 13 | } 14 | .desc { 15 | margin: 20px 0; 16 | font-size: @font-size-medium-x; 17 | font-weight: @font-weight-small; 18 | color: @color-base; 19 | } 20 | .login { 21 | margin: 0 auto; 22 | display: block; 23 | background-color: #4cd96f; 24 | border-radius: 8px; 25 | outline: none; 26 | border: none; 27 | width: 600px; 28 | height: 84px; 29 | line-height: 84px; 30 | margin-top: 40px; 31 | font-size: @font-size-medium-x; 32 | font-weight: @font-weight-small; 33 | color:#fff; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/assets/svg/right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/modal/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | import PropTypes from 'prop-types' 6 | import QueueAnim from 'rc-queue-anim' 7 | 8 | export default class Modal extends React.Component { 9 | static propTypes = { 10 | visible: PropTypes.bool, 11 | } 12 | 13 | constructor(props) { 14 | super(props) 15 | this.el = document.createElement('div') 16 | } 17 | 18 | componentDidMount() { 19 | document.body.appendChild(this.el) 20 | } 21 | 22 | componentWillUnmount() { 23 | document.body.removeChild(this.el) 24 | } 25 | render() { 26 | const { visible, children } = this.props 27 | return ReactDOM.createPortal( 28 | 29 | { 30 | visible ? children : null 31 | } 32 | , 33 | this.el, 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/scroll/index.less: -------------------------------------------------------------------------------- 1 | 2 | @import '../../assets/css/theme.less'; 3 | @import '../../assets/css/mixin.less'; 4 | 5 | .list-wrapper { 6 | position: absolute; 7 | .position-full; 8 | overflow: hidden; 9 | background-color: @fill-body; 10 | .scroll-content { 11 | position: relative; 12 | z-index: 10; 13 | } 14 | .pullup-wrapper { 15 | width: 100%; 16 | padding-top: 20px; 17 | padding-bottom: 20px; 18 | .flex-center; 19 | } 20 | .after-trigger,.before-trigger,.refresh-txt { 21 | .flex-center; 22 | span { 23 | padding: 5px 0; 24 | font-size: @font-size-medium; 25 | font-weight: @font-weight-small; 26 | color: @color-base; 27 | } 28 | } 29 | .pulldown-wrapper { 30 | position: absolute; 31 | z-index: 1; 32 | width: 100%; 33 | left: 0; 34 | .flex-center; 35 | transition: all 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/home/index.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | @import '../../assets/css/theme.less'; 4 | @import '../../assets/css/mixin.less'; 5 | 6 | .root { 7 | width: 100%; 8 | height: 100%; 9 | position: relative; 10 | z-index: 1; 11 | .scroll { 12 | z-index: 1; 13 | overflow: visible; 14 | } 15 | .entry-wrapper { 16 | background-color: @fill-body; 17 | display: flex; 18 | flex-wrap: wrap; 19 | margin-bottom: 30px; 20 | .item { 21 | width: 20vw; 22 | margin-top: 22px; 23 | .flex-center; 24 | flex-direction: column; 25 | .img { 26 | width: 90px; 27 | height: 90px; 28 | img { 29 | width: 100%; 30 | } 31 | } 32 | .name { 33 | margin-top: 10px; 34 | font-size: @font-size-small; 35 | font-weight: @font-weight-small; 36 | color: @color-base; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/assets/svg/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/common-components/nav-bar/index.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | @import '../../../assets/css/mixin.less'; 4 | @import '../../../assets/css/theme.less'; 5 | 6 | .nav { 7 | width: 100%; 8 | height: @bar-height; 9 | background-image: linear-gradient(-90deg, @primary-color, @primary-color-light); 10 | box-sizing: border-box; 11 | display: flex; 12 | justify-content: space-between; 13 | align-items: center; 14 | position: relative; 15 | .icon { 16 | flex: 0 0 @bar-height; 17 | width: @bar-height; 18 | height: @bar-height; 19 | display: flex; 20 | .flex-center; 21 | font-size: @font-size-large-x; 22 | color: #fff; 23 | } 24 | .title { 25 | position: absolute; 26 | left: 50%; 27 | top: 0; 28 | height: @bar-height; 29 | line-height: @bar-height; 30 | font-size: @font-size-medium-x; 31 | transform: translateX(-50%); 32 | color: #fff; 33 | text-align: center; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/assets/css/theme.less: -------------------------------------------------------------------------------- 1 | 2 | @hd: 2; 3 | @tab-theme: #f8f7f5; 4 | @tab-height: 100px; 5 | 6 | @bar-height: 100px; 7 | @item-height: 88px; 8 | @input-height: 96px; 9 | 10 | @shop-cart-height: 140px; 11 | 12 | // @white-space-height: 20px; 13 | // @white-space-color: #f4f4f4; 14 | 15 | 16 | @skeleton-color: #f3f3f3; 17 | @skeleton-color-darken: #e6e6e6; 18 | @hairline-color: #eee; 19 | 20 | @badge-color-open: rgb(84, 206, 117); 21 | @badge-color-first: rgb(112, 188, 70); // 首单color 22 | 23 | @border-color: #eee; 24 | 25 | @primary-color: #0085ff; 26 | @primary-color-light: #0af; 27 | 28 | @color-title: #333; 29 | @color-base: #666; 30 | @fill-body: #fff; 31 | @fill-body-darken: #f5f5f5; 32 | 33 | @font-weight-small: 300; 34 | @font-weight-medium: 400; 35 | 36 | @font-size-small-s: 10px * @hd; 37 | @font-size-small: 12px * @hd; 38 | @font-size-medium: 14px * @hd; 39 | @font-size-medium-x: 16px * @hd; 40 | @font-size-large: 18px * @hd; 41 | @font-size-large-x: 22px * @hd; 42 | -------------------------------------------------------------------------------- /src/assets/svg/filter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/address/address-row/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import SvgIcon from 'components/icon-svg' 5 | import config from 'utils/config' 6 | import styles from './index.less' 7 | 8 | export default class AddressRow extends React.PureComponent { 9 | render() { 10 | const { data } = this.props 11 | const { sexMap } = config 12 | return ( 13 |
14 |
15 |
16 |

{data.name}

17 | {sexMap[data.sex]} 18 | {data.phone} 19 |
20 |

{data.address + data.address_detail}

21 |
22 |
23 | 24 |
25 |
26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/assets/svg/check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/iphone.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/crown.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | /*eslint-disable*/ 3 | import React from 'react' 4 | import ReactDOM from 'react-dom' 5 | import { Provider } from 'react-redux'; 6 | import { BrowserRouter } from 'react-router-dom' 7 | import { AppContainer } from 'react-hot-loader' 8 | import store from './stores' 9 | import App from './pages' 10 | import './assets/css/common.less' 11 | 12 | // requires and returns all modules that match 13 | const requireAll = requireContext => requireContext.keys().map(requireContext) 14 | // import all svg 15 | const reqSvg = require.context('./assets/svg', true, /\.svg$/) 16 | requireAll(reqSvg) 17 | 18 | const render = Component => ( 19 | ReactDOM.render(( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ), document.getElementById('root')) 28 | ) 29 | 30 | render(App) 31 | 32 | if (module.hot) { 33 | module.hot.accept('./pages', () => { 34 | render(App) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /src/assets/svg/location.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/shop-detail/foods/menu.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import cls from 'classnames' 5 | import { connect } from 'react-redux' 6 | import Scroll from 'components/scroll' 7 | import styles from './index.less' 8 | 9 | const mapStateToProps = ({ shop }) => ({ 10 | menu: shop.menu, 11 | foodMenuIndex: shop.foodMenuIndex, 12 | }) 13 | @connect(mapStateToProps) 14 | export default class Menu extends React.PureComponent { 15 | render() { 16 | const { menu, foodMenuIndex, menuClick } = this.props 17 | const menuCls = v => cls([styles.item, v === foodMenuIndex ? styles.active : null]) 18 | return ( 19 |
20 | 21 | { 22 | menu.map((v, i) => ( 23 |
menuClick(i)}> 24 | {v.name} 25 |
26 | )) 27 | } 28 |
29 |
30 | ) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/slide/index.less: -------------------------------------------------------------------------------- 1 | 2 | .slide { 3 | min-height: 1px; 4 | position: relative; 5 | overflow: hidden; 6 | .slide-group { 7 | white-space: nowrap; 8 | .slide-item { 9 | float: left; 10 | box-sizing: border-box; 11 | overflow: hidden; 12 | text-align: center; 13 | a { 14 | display: block; 15 | width: 100%; 16 | overflow: hidden; 17 | text-decoration: none; 18 | } 19 | img { 20 | display: block; 21 | width: 100%; 22 | } 23 | } 24 | } 25 | .dots { 26 | position: absolute; 27 | right: 0; 28 | left: 0; 29 | bottom: 12px; 30 | transform: translateZ(1px); 31 | text-align: center; 32 | font-size: 0; 33 | .dot { 34 | display: inline-block; 35 | margin: 0 4px; 36 | width: 12px; 37 | height: 12px; 38 | border-radius: 50%; 39 | background-color: rgba(255, 255, 255, .5); 40 | &.active { 41 | width: 20px; 42 | border-radius: 5px; 43 | background-color: #f95108; 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | eleme 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/components/loading/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | /* eslint-disable */ 3 | import React from 'react' 4 | import Proptypes from 'prop-types' 5 | import loading from './loading.gif' 6 | import styles from './index.less' 7 | 8 | export default class Loading extends React.Component { 9 | static proptypes = { 10 | title: Proptypes.string, 11 | style: Proptypes.object, 12 | } 13 | 14 | static defaultProps = { 15 | title: '', 16 | style: {}, 17 | } 18 | 19 | constructor(props) { 20 | super(props) 21 | this.ratio = window.devicePixelRatio 22 | this.state = { 23 | width: 14 * this.ratio, 24 | height: 14 * this.ratio, 25 | } 26 | } 27 | 28 | render() { 29 | const { width, height } = this.state 30 | const { title, style } = this.props 31 | return ( 32 |
33 | 38 |

正在加载...

39 |
40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/pages/address/address-row/index.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | @import '../../../assets/css/theme.less'; 4 | @import '../../../assets/css/mixin.less'; 5 | 6 | .row { 7 | width: 100%; 8 | box-sizing: border-box; 9 | padding: 30px; 10 | background-color: @fill-body; 11 | border-bottom: 1px solid @border-color; 12 | display: flex; 13 | .desc { 14 | flex: 1; 15 | width: 0; 16 | .info { 17 | display: flex; 18 | align-items: center; 19 | height: 50px; 20 | line-height: 50px; 21 | .name { 22 | font-size: @font-size-medium-x; 23 | color: @color-title; 24 | } 25 | .sex { 26 | margin: 0 10px; 27 | } 28 | .sex,.phone { 29 | font-size: @font-size-medium; 30 | } 31 | } 32 | .address { 33 | font-size: @font-size-small; 34 | font-weight: @font-weight-small; 35 | line-height: 34px; 36 | } 37 | } 38 | .edit { 39 | flex: 0 0 100px; 40 | width: 100px; 41 | display: flex; 42 | justify-content: flex-end; 43 | align-items: center; 44 | font-size: @font-size-large-x; 45 | color: #999; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 gaojingran960611 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 | -------------------------------------------------------------------------------- /src/assets/svg/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/stores/order.js: -------------------------------------------------------------------------------- 1 | 2 | import Toast from 'components/toast' 3 | import { getOrderList } from '../api' 4 | 5 | const UPDATE = 'ORDER_UPDATE' 6 | 7 | const initState = { 8 | init: false, 9 | orderList: [], 10 | } 11 | 12 | export const order = (state = initState, action) => { 13 | switch (action.type) { 14 | case UPDATE: 15 | return { 16 | ...state, 17 | ...action.payload, 18 | } 19 | default: 20 | return state 21 | } 22 | } 23 | 24 | export const orderUpdate = (params) => { 25 | return { 26 | payload: params, 27 | type: UPDATE, 28 | } 29 | } 30 | 31 | export const fetchOrderList = (isRefresh, callback) => { 32 | return async (dispatch, getState) => { 33 | const { orderList } = getState().order 34 | try { 35 | const { data } = await getOrderList({ 36 | limit: 8, 37 | offset: isRefresh ? 0 : orderList.length, 38 | }) 39 | dispatch(orderUpdate({ 40 | init: true, 41 | orderList: isRefresh ? data : [...orderList, ...data], 42 | })) 43 | callback && callback() // eslint-disable-line 44 | } catch ({ err }) { 45 | Toast.info(err, 3, false) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/api/http.js: -------------------------------------------------------------------------------- 1 | 2 | import axios from 'axios' 3 | 4 | const successCode = 0 5 | const instance = axios.create({ 6 | baseURL: '/api', 7 | withCredentials: true, // 跨域类型时是否在请求中协带cookie 8 | }) 9 | 10 | export default class HttpUtil { 11 | static get(url, params = {}) { 12 | return new Promise((resolve, reject) => { 13 | instance.get(url, { params }).then(({ data }) => { 14 | if (data.code === successCode) { 15 | const { result } = data 16 | resolve({ data: result }) 17 | } else { 18 | reject({ err: data.errmsg, name: data.name || '' }) 19 | } 20 | }).catch((err) => { 21 | reject({ err: JSON.stringify(err) }) 22 | }) 23 | }) 24 | } 25 | 26 | static post(url, params = {}) { 27 | return new Promise((resolve, reject) => { 28 | instance.post(url, { data: params }).then(({ data }) => { 29 | if (data.code === successCode) { 30 | const { result } = data 31 | resolve({ data: result }) 32 | } else { 33 | reject({ err: data.errmsg, name: data.name || '' }) 34 | } 35 | }).catch((err) => { 36 | reject({ err: JSON.stringify(err) }) 37 | }) 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/assets/svg/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/shop-detail/skeleton-screen/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import Row from '../../common-components/skeleton/row' 5 | import styles from './index.less' 6 | 7 | export default class Skeleton extends React.PureComponent { 8 | render() { 9 | return ( 10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | { 20 | Array.from({ length: 9 }, (v, i) => i + 1).map(v => ( 21 |
22 | 23 |
24 | )) 25 | } 26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 |
36 |
37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/assets/css/mixin.less: -------------------------------------------------------------------------------- 1 | 2 | .no-wrap { 3 | overflow: hidden; 4 | text-overflow: ellipsis; 5 | white-space: nowrap; 6 | word-break: break-all; 7 | } 8 | 9 | .extend-click { 10 | position: relative; 11 | &:before { 12 | content: '' !important; 13 | position: absolute; 14 | top: -10px; 15 | left: -10px; 16 | right: -10px; 17 | bottom: -10px; 18 | } 19 | } 20 | 21 | .clearfix { 22 | display: inline-block; 23 | &:after { 24 | display: block; 25 | content: '' !important; 26 | height: 0; 27 | line-height: 0; 28 | clear: both; 29 | visibility: hidden; 30 | } 31 | } 32 | 33 | .position-full { 34 | top: 0; 35 | left: 0; 36 | right: 0; 37 | bottom: 0; 38 | } 39 | 40 | .flex-center { 41 | display: flex; 42 | align-items: center; 43 | justify-content: center; 44 | } 45 | 46 | .placeholder-color(@color) { 47 | input::-webkit-input-placeholder, textarea::-webkit-input-placeholder { 48 | color: @color; 49 | } 50 | input:-moz-placeholder, textarea:-moz-placeholder { 51 | color: @color; 52 | } 53 | input::-moz-placeholder, textarea::-moz-placeholder { 54 | color: @color; 55 | } 56 | input:-ms-input-placeholder, textarea:-ms-input-placeholder { 57 | color: @color; 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/pages/address/index.less: -------------------------------------------------------------------------------- 1 | @import '../../assets/css/theme.less'; 2 | @import '../../assets/css/mixin.less'; 3 | 4 | @btn-height: 100px; 5 | 6 | .address { 7 | width: 100%; 8 | height: 100%; 9 | background-color: @fill-body-darken; 10 | position: relative; 11 | z-index: 1; 12 | .list { 13 | position: fixed; 14 | top: @bar-height; 15 | left: 0; 16 | right: 0; 17 | bottom: @btn-height; 18 | .scroll { 19 | background-color: @fill-body-darken; 20 | } 21 | } 22 | .add { 23 | position: fixed; 24 | z-index: 1; 25 | bottom: 0; 26 | left: 0; 27 | width: 100%; 28 | height: @btn-height; 29 | line-height: @btn-height; 30 | background-color: @fill-body; 31 | box-sizing: border-box; 32 | padding: 0; 33 | border: none; 34 | border-top: 1px solid @border-color; 35 | font-size: 0; 36 | color: @primary-color-light; 37 | .icon { 38 | display: inline-block; 39 | vertical-align: middle; 40 | font-size: @font-size-large; 41 | margin-right: 8px; 42 | margin-top: -4px; 43 | } 44 | span { 45 | display: inline-block; 46 | vertical-align: middle; 47 | font-size: @font-size-medium-x; 48 | font-weight: @font-weight-small; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/pages/common-components/gallery/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import PropTypes from 'prop-types' 5 | import Modal from 'components/modal' 6 | import Slide from 'components/slide' 7 | import styles from './index.less' 8 | 9 | export default class Gallery extends React.Component { 10 | static propTypes = { 11 | visible: PropTypes.bool, 12 | handleCancel: PropTypes.func, 13 | photos: PropTypes.array, 14 | } 15 | 16 | render() { 17 | const { visible, photos, handleCancel = () => {} } = this.props 18 | return ( 19 | 20 |
21 |
22 | { 23 | photos.length ? ( 24 | 1} 26 | showDot={false} 27 | autoPlay={false}> 28 | { 29 | photos.map((v, i) => ( 30 | 31 | )) 32 | } 33 | 34 | ) : null 35 | } 36 |
37 |
38 |
39 | 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/assets/svg/delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/common-components/recommend-food-row/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import SvgIcon from 'components/icon-svg' 5 | import { getImageUrl } from 'utils/utils' 6 | import styles from './index.less' 7 | 8 | export default class RecommedFoodRow extends React.PureComponent { 9 | render() { 10 | const { data, rowClick } = this.props 11 | const { food, restaurant } = data 12 | const foodUrl = getImageUrl(food.image_path) 13 | return ( 14 |
15 |
16 | 17 |

{food.reason}

18 |
19 |
20 |

{food.name}

21 |

{`月售${food.month_sales} 好评率${food.satisfy_rate}%`}

22 |

23 | ¥ 24 | {food.price} 25 |

26 |
27 | 28 |

{restaurant.name}

29 |
30 |
31 |
32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/event-proxy.js: -------------------------------------------------------------------------------- 1 | 2 | // http://taobaofed.org/blog/2016/11/17/react-components-communication/ 3 | /*eslint-disable*/ 4 | const eventProxy = { 5 | onObj: {}, 6 | oneObj: {}, 7 | on: function(key, fn) { 8 | if(this.onObj[key] === undefined) { 9 | this.onObj[key] = [] 10 | } 11 | this.onObj[key].push(fn) 12 | }, 13 | one: function(key, fn) { 14 | if(this.oneObj[key] === undefined) { 15 | this.oneObj[key] = [] 16 | } 17 | this.oneObj[key].push(fn) 18 | }, 19 | off: function(key) { 20 | this.onObj[key] = [] 21 | this.oneObj[key] = [] 22 | }, 23 | trigger: function() { 24 | let key, args 25 | if(arguments.length == 0) { 26 | return false 27 | } 28 | key = arguments[0] 29 | args = [].concat(Array.prototype.slice.call(arguments, 1)) 30 | 31 | if(this.onObj[key] !== undefined && this.onObj[key].length > 0) { 32 | for(let i in this.onObj[key]) { 33 | this.onObj[key][i].apply(null, args) 34 | } 35 | } 36 | if(this.oneObj[key] !== undefined && this.oneObj[key].length > 0) { 37 | for(let i in this.oneObj[key]) { 38 | this.oneObj[key][i].apply(null, args) 39 | this.oneObj[key][i] = undefined 40 | } 41 | this.oneObj[key] = [] 42 | } 43 | } 44 | } 45 | 46 | export default eventProxy 47 | -------------------------------------------------------------------------------- /src/assets/svg/round_add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/.eslintrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "parser": "babel-eslint", 4 | "extends": "airbnb", 5 | "globals": { 6 | "document": true, 7 | "AMap": true, 8 | "window": true 9 | }, 10 | "rules": { 11 | "semi": [0], 12 | "camelcase": [0], 13 | "no-plusplus": [0], 14 | "consistent-return": [0], 15 | "arrow-body-style": [0], 16 | "react/prop-types": [0], 17 | "no-unneeded-ternary": [0], 18 | "react/no-find-dom-node": [0], 19 | "react/jsx-filename-extension": [0], 20 | "array-callback-return": [0], 21 | "no-mixed-operators": [0], 22 | "no-nested-ternary": [0], 23 | "jsx-a11y/alt-text": [0], 24 | "jsx-a11y/anchor-is-valid": [0], 25 | "react/forbid-prop-types": [0], 26 | "react/require-default-props": [0], 27 | "class-methods-use-this": [0], 28 | "prefer-promise-reject-errors": [0], 29 | "jsx-a11y/click-events-have-key-events": [0], 30 | "jsx-a11y/no-noninteractive-element-interactions": [0], 31 | "react/no-array-index-key": [0], 32 | "import/no-extraneous-dependencies": [0], 33 | "import/no-unresolved": [0], 34 | "import/extensions": [0], 35 | "react/jsx-closing-bracket-location": [0], 36 | "react/prefer-stateless-function": [0], 37 | "jsx-a11y/no-static-element-interactions": [0], 38 | "react/jsx-boolean-value": [0], 39 | "no-console": [0], 40 | "no-return-assign": [0] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/pages/common-components/tab-bar/index.less: -------------------------------------------------------------------------------- 1 | 2 | @import '../../../assets/css/mixin.less'; 3 | @import '../../../assets/css/theme.less'; 4 | 5 | .root { 6 | position: fixed; 7 | .position-full; 8 | overflow: hidden; 9 | z-index: 1; 10 | background-color: @fill-body; 11 | padding-bottom: @tab-height; 12 | padding-bottom: calc(@tab-height + constant(safe-area-inset-bottom)); 13 | padding-bottom: calc(@tab-height + env(safe-area-inset-bottom)); 14 | } 15 | 16 | .tab-wrapper { 17 | position: fixed; 18 | z-index: 1; 19 | left: 0; 20 | right: 0; 21 | height: @tab-height; 22 | padding-bottom: constant(safe-area-inset-bottom); 23 | padding-bottom: env(safe-area-inset-bottom); 24 | background-color: @tab-theme; 25 | box-shadow: 0 -0.5px 2px rgba(0,0,0,.1); 26 | .flex-center; 27 | .item { 28 | flex: 1; 29 | flex-direction: column; 30 | .flex-center; 31 | .icon { 32 | width: 44px; 33 | height: 44px; 34 | fill: @color-base; 35 | &.scale { 36 | transform: scale(1.67); 37 | transform-origin: center; 38 | } 39 | } 40 | .text { 41 | font-weight: @font-weight-small; 42 | font-size: @font-size-small-s; 43 | color: @color-base; 44 | margin-top: 8px; 45 | } 46 | &.active { 47 | .icon { 48 | fill: @primary-color; 49 | } 50 | .text { 51 | color: @primary-color; 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/assets/svg/success.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | /*eslint-disable*/ 4 | /*** 5 | * array = [ 6 | * { a: 1 } 7 | * { a: 1 } 8 | * { b: 1 } 9 | * ] 10 | */ 11 | export const unique = (array, key) => { 12 | const filter = array.reduce((acc, val) => { 13 | acc[val[key]] = val; 14 | return acc 15 | }, {}) 16 | let result = [] 17 | for (let key in filter) { 18 | result.push(filter[key]) 19 | } 20 | return result 21 | } 22 | 23 | export const getGuid = (len = 8, radix = 2) => { 24 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 25 | const r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); 26 | return v.toString(16); 27 | }); 28 | } 29 | 30 | export const formatPhone = (phone) => { 31 | return phone.substr(0, 3) + '****' + phone.substr(7, 11) 32 | } 33 | 34 | const baseUrl = '//fuss10.elemecdn.com' 35 | export const getImageUrl = (hash) => { 36 | if (!hash) { 37 | return null 38 | } 39 | const suffixArray = ['png', 'bmp', 'jpg', 'gif', 'jpeg', 'svg'] 40 | const suffix = suffixArray.find(v => hash.indexOf(v) !== -1) 41 | if (!suffix) { 42 | return null 43 | } else { 44 | return `${baseUrl}/${hash.substring(0,1)}/${hash.substring(1,3)}/${hash.substring(3)}.${suffix}` 45 | } 46 | } 47 | 48 | export const debounce = function(fn, interval = 600) { 49 | let timeout = null 50 | return function() { 51 | clearTimeout(timeout) 52 | timeout = setTimeout(() => { 53 | fn.apply(this, arguments) 54 | }, interval) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/assets/svg/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/pages/common-components/red-ticket-row/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import SvgIcon from 'components/icon-svg' 5 | import styles from './index.less' 6 | 7 | export default class RedTicketRow extends React.PureComponent { 8 | render() { 9 | const { data, rowClick } = this.props 10 | return ( 11 |
12 |
13 | 14 |
15 |

16 | ¥ 17 | {data.amount} 18 |

19 |

{data.description_map.sum_condition}

20 |
21 |
22 |

{data.name}

23 |

{data.description_map.validity_periods}

24 |

{data.description_map.phone}

25 |
26 |
27 |

{data.description_map.validity_delta}

28 | 29 |
30 |
31 |
32 |

33 | {data.limit_map.restaurant_flavor_ids} 34 |

35 |
36 |
37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/pages/common-components/nav-bar/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import Proptypes from 'prop-types' 5 | import cls from 'classnames' 6 | import SvgIcon from 'components/icon-svg' 7 | import styles from './index.less' 8 | 9 | export default class NavBar extends React.PureComponent { 10 | /* eslint-disable */ 11 | static proptypes = { 12 | iconLeft: Proptypes.string, 13 | iconRight: Proptypes.string, 14 | leftClick: Proptypes.func, 15 | rightClick: Proptypes.func, 16 | title: Proptypes.string, 17 | className: Proptypes.string, 18 | } 19 | /* eslint-enable */ 20 | 21 | static defaultProps = { 22 | iconLeft: '', 23 | iconRight: '', 24 | leftClick: () => {}, 25 | rightClick: () => {}, 26 | title: '', 27 | } 28 | 29 | render() { 30 | const { 31 | iconLeft, 32 | iconRight, 33 | leftClick, 34 | rightClick, 35 | title, 36 | className, 37 | } = this.props 38 | return ( 39 |
40 | { 41 | iconLeft ? ( 42 |
43 | 44 |
45 | ) : null 46 | } 47 |
{title}
48 | { 49 | iconRight ? ( 50 |
51 | 52 |
53 | ) : null 54 | } 55 |
56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/assets/svg/avatar.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/pages/home/top-bar/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import { connect } from 'react-redux' 5 | import { bindActionCreators } from 'redux' 6 | import { withRouter } from 'react-router-dom' 7 | import cls from 'classnames' 8 | import SvgIcon from 'components/icon-svg' 9 | import { homeUpdate } from '../../../stores/home' 10 | import styles from './index.less' 11 | 12 | const mapStateToProps = ({ home }) => ({ 13 | topBarShrink: home.topBarShrink, 14 | locationInfo: home.locationInfo, 15 | }) 16 | const mapActionsToProps = dispatch => bindActionCreators({ homeUpdate }, dispatch) 17 | 18 | @connect(mapStateToProps, mapActionsToProps) 19 | @withRouter 20 | export default class TopBar extends React.PureComponent { 21 | render() { 22 | const { topBarShrink, history } = this.props 23 | const { address } = this.props.locationInfo 24 | const clsname = cls({ 25 | [styles.header]: true, 26 | [styles.shrink]: topBarShrink, 27 | }) 28 | return ( 29 |
30 |
history.push('/search-address')}> 31 | 32 |

{address ? address : '正在识别地址...'}

33 | 34 |
35 |
history.push('/search-shop')}> 36 | 37 |

搜索饿了么商家、商品名称

38 |
39 |
40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/toast/index.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | @maskIndex: 1000; 4 | @noticeIndex: 1100; 5 | @toast-bg-color: rgba(0, 0, 0, .7); 6 | @font-size: 28px; 7 | @line-height: 34px; 8 | 9 | .mask { 10 | position: fixed; 11 | z-index: @maskIndex; 12 | top: 0; 13 | left: 0; 14 | right: 0; 15 | bottom: 0; 16 | } 17 | 18 | .notification-container { 19 | .notification-box { 20 | .notice-container { 21 | position: absolute; 22 | z-index: @noticeIndex; 23 | top: 30%; 24 | left: 50%; 25 | padding: 20px 30px; 26 | max-width: 500px; 27 | white-space: pre-wrap; 28 | word-break: break-all; 29 | background-color: @toast-bg-color; 30 | color: #fff; 31 | font-size: @font-size; 32 | line-height: @line-height; 33 | text-align: center; 34 | border-radius: 10px; 35 | 36 | transition: all .3s; 37 | transform: translate3D(-50%, -50%, 0) scale(1, 1); 38 | 39 | animation: slideInFromBottom .3s linear; 40 | &.leave { 41 | transform: translate3D(-50%, -100%, 0); 42 | opacity: 0; 43 | } 44 | 45 | .notice { 46 | .icon { 47 | width: 60px; 48 | height: 60px; 49 | fill: #fff; 50 | margin-bottom: 12px; 51 | &.loading { 52 | animation: rotation 1s linear infinite; 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | // 从底部滑入 61 | @keyframes slideInFromBottom { 62 | 0% { 63 | transform: translate(-50%, 0%); 64 | opacity: 0; 65 | } 66 | 100% { 67 | transform: translate(-50%, -50%); 68 | opacity: 1; 69 | } 70 | } 71 | 72 | @keyframes rotation { 73 | to { 74 | transform: rotate(360deg); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/stores/compass.js: -------------------------------------------------------------------------------- 1 | 2 | import Toast from 'components/toast' 3 | import { getRecommendation, getGeolocation } from '../api' 4 | import { homeUpdate } from './home' 5 | 6 | const UPDATE = 'COMPASS_UPDATE' 7 | 8 | const initState = { 9 | init: false, 10 | foodList: [], 11 | rank_id: undefined, 12 | } 13 | 14 | export const compass = (state = initState, action) => { 15 | switch (action.type) { 16 | case UPDATE: 17 | return { 18 | ...state, 19 | ...action.payload, 20 | } 21 | default: 22 | return state 23 | } 24 | } 25 | 26 | export const compassUpdate = (params) => { 27 | return { 28 | payload: params, 29 | type: UPDATE, 30 | } 31 | } 32 | 33 | export const fetchFoodList = () => { 34 | return async (dispatch, getState) => { 35 | const { foodList, rank_id } = getState().compass 36 | let { locationInfo } = getState().home 37 | if (!locationInfo.latitude && !locationInfo.longitude) { 38 | try { 39 | const { data } = await getGeolocation() 40 | if (data) { 41 | locationInfo = data 42 | dispatch(homeUpdate({ 43 | locationInfo: data, 44 | })) 45 | } 46 | } catch ({ err }) { 47 | return Toast.info(err) 48 | } 49 | } 50 | try { 51 | const { data } = await getRecommendation({ 52 | rank_id, 53 | limit: 20, 54 | offset: foodList.length, 55 | latitude: locationInfo.latitude, 56 | longitude: locationInfo.longitude, 57 | }) 58 | dispatch(compassUpdate({ 59 | init: true, 60 | rank_id: data.rank_id, 61 | foodList: [...foodList, ...data.items], 62 | })) 63 | } catch ({ err }) { 64 | return Toast.info(err) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/pages/common-components/tab-bar/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import cls from 'classnames' 5 | import SvgIcon from 'components/icon-svg' 6 | import styles from './index.less' 7 | 8 | export default (Component) => { 9 | return class TabBar extends React.Component { 10 | render() { 11 | const { pathname } = this.props.location 12 | const itemCls = name => cls({ 13 | [styles.item]: true, 14 | [styles.active]: pathname === name, 15 | }) 16 | const handleClick = (path) => { 17 | if (path === pathname) return 18 | this.props.history.push(path) 19 | } 20 | return ( 21 |
22 | 23 |
24 |
handleClick('/home')}> 25 | 26 |

微淘

27 |
28 |
handleClick('/compass')}> 29 | 30 |

发现

31 |
32 |
handleClick('/order')}> 33 | 34 |

订单

35 |
36 |
handleClick('/profile')}> 37 | 38 |

我的

39 |
40 |
41 |
42 | ) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/pages/search-shop/list.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import { connect } from 'react-redux' 5 | import { withRouter } from 'react-router-dom' 6 | import Loading from 'components/loading' 7 | import Scroll from 'components/scroll' 8 | import NoData from '../common-components/no-data' 9 | import ShopListRow from '../common-components/shop-list-row' 10 | import styles from './index.less' 11 | 12 | @connect(({ searchShop, home }) => ({ 13 | shopLists: searchShop.shopLists, 14 | loading: searchShop.loading, 15 | locationInfo: home.locationInfo, 16 | })) 17 | @withRouter 18 | export default class ShopList extends React.PureComponent { 19 | refreshScroll = () => { 20 | this.scroll && this.scroll.refresh() // eslint-disable-line 21 | } 22 | 23 | handleRowClick = (val) => { 24 | const { history, locationInfo } = this.props 25 | const { id } = val 26 | const { latitude, longitude } = locationInfo 27 | history.push({ 28 | pathname: '/shop-detail', 29 | search: `?restaurant_id=${id}&latitude=${latitude}&longitude=${longitude}`, 30 | }) 31 | } 32 | 33 | render() { 34 | const { shopLists, loading } = this.props 35 | return ( 36 |
37 | { 38 | loading ? : shopLists.length ? ( 39 | this.scroll = c}> 40 | { 41 | shopLists.map((shop, i) => ( 42 | this.handleRowClick(shop)} 44 | key={`${shop.id}--${i}--${new Date().getTime()}`} 45 | data={shop} 46 | refresh={this.refreshScroll} /> 47 | )) 48 | } 49 | 50 | ) : 51 | } 52 |
53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/assets/svg/elem.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/pages/address-nearby/index.less: -------------------------------------------------------------------------------- 1 | 2 | @import '../../assets/css/theme.less'; 3 | @import '../../assets/css/mixin.less'; 4 | 5 | .address { 6 | position: fixed; 7 | .position-full; 8 | background-color: @fill-body; 9 | .list { 10 | position: fixed; 11 | top: @bar-height; 12 | left: 0; 13 | right: 0; 14 | bottom: 0; 15 | overflow: hidden; 16 | .search { 17 | padding: 20px 30px 4px 30px; 18 | display: flex; 19 | align-items: center; 20 | .input { 21 | flex: 1; 22 | box-sizing: border-box; 23 | border-radius: 4px; 24 | overflow: hidden; 25 | background-color: #f5f5f5; 26 | border: 1px solid @border-color; 27 | height: 58px; 28 | line-height: 58px; 29 | box-sizing: border-box; 30 | padding: 0 20px 0 60px; 31 | position: relative; 32 | .icon { 33 | position: absolute; 34 | left: 14px; 35 | top: 50%; 36 | transform: translateY(-50%); 37 | font-size: @font-size-medium; 38 | color: @color-base; 39 | } 40 | input { 41 | display: block; 42 | background-color: transparent; 43 | font-size: @font-size-small; 44 | font-weight: @font-weight-small; 45 | color: @color-base; 46 | outline: none; 47 | width: 100%; 48 | line-height: 58px; 49 | } 50 | } 51 | .btn { 52 | flex: 0 0 120px; 53 | width: 120px; 54 | height: 58px; 55 | margin-left: 20px; 56 | background-color: @primary-color-light; 57 | color: #fff; 58 | font-size: @font-size-medium; 59 | line-height: 58px; 60 | border-radius: 4px; 61 | outline: none; 62 | border: none; 63 | } 64 | } 65 | .container { 66 | box-sizing: border-box; 67 | padding-left: 30px; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/utils/dom.js: -------------------------------------------------------------------------------- 1 | 2 | /*eslint-disable*/ 3 | export const hasClass = (el, className) => { 4 | if (el.classList) { 5 | return el.classList.contains(className) 6 | } 7 | const nameArray = el.className.split(' ') 8 | return nameArray.indexOf(className) > -1 9 | } 10 | 11 | export const addClass = (el, className) => { 12 | if (el.classList) { 13 | el.classList.add(className) 14 | } else { 15 | const newClass = el.className.split(' ').concat([className]).join(' ') 16 | el.className = newClass 17 | } 18 | } 19 | 20 | export const removeClass = (el, className) => { 21 | if (el.classList) { 22 | el.classList.remove(className) 23 | } else { 24 | if (hasClass(el, className)) { 25 | const newClass = el.className.split(' ').filter(name => name !== className).join(' ') 26 | el.className = newClass 27 | } 28 | } 29 | } 30 | 31 | // css3 hark前缀 32 | const elementStyle = document.createElement('div').style 33 | const vendor = (() => { 34 | const transformNames = { 35 | Webkit: 'webkitTransform', 36 | Moz: 'MozTransform', 37 | O: 'OTransform', 38 | ms: 'msTransform', 39 | standard: 'transform', 40 | } 41 | for (let key in transformNames) { 42 | if (elementStyle[transformNames[key]] !== undefined) { 43 | return key 44 | } 45 | } 46 | return false 47 | })() 48 | 49 | export const prefixStyle = (style) => { 50 | if (!vendor) return false 51 | if (vendor === 'standard') { 52 | return style 53 | } 54 | return vendor + style.charAt(0).toUpperCase() + style.substr(1) 55 | } 56 | 57 | export const watchTransitionEvent = () => { 58 | const transitions = { 59 | transition: 'transitionend', 60 | OTransition: 'oTransitionEnd', 61 | MozTransition: 'transitionend', 62 | WebkitTransition: 'webkitTransitionEnd', 63 | }; 64 | for (let key in transitions) { 65 | if (elementStyle[key] !== undefined) { 66 | return transitions[key]; 67 | } 68 | } 69 | return false; 70 | }; 71 | -------------------------------------------------------------------------------- /src/pages/common-components/order-list-row/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import numeral from 'numeral' 5 | import SvgIcon from 'components/icon-svg' 6 | import styles from './index.less' 7 | 8 | export default class OrderRow extends React.PureComponent { 9 | render() { 10 | const { data, handleClick } = this.props 11 | 12 | return ( 13 |
14 |
15 |
16 | 17 |
18 | 19 |
20 |
21 |
22 |

{data.restaurant_name}

23 |
24 | 25 |
26 |
27 |

{data.status_bar.title}

28 |
29 | 30 |

{data.formatted_created_at}

31 |
32 | 33 |
34 |

35 | {data.basket.group[0][0].name} 36 | { 37 | data.basket.group[0].length ? `等${data.basket.group[0].length}商品` : '' 38 | } 39 |

40 |

¥{numeral(data.total_amount).format('0.00')}

41 |
42 |
43 |
44 | 45 |
46 | 47 |
48 | 49 |
50 |
51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/pages/shop-detail/skeleton-screen/index.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | @import '../../../assets/css/theme.less'; 4 | @import '../../../assets/css/mixin.less'; 5 | 6 | @header-height: 424px; // 商家信息 7 | 8 | .skeleton { 9 | position: fixed; 10 | .position-full; 11 | background-color: @fill-body; 12 | overflow: hidden; 13 | .header { 14 | width: 100%; 15 | height: @header-height; 16 | background-color: @skeleton-color; 17 | display: flex; 18 | flex-direction: column; 19 | 20 | .avatar { 21 | width: 130px; 22 | height: 130px; 23 | margin: 0 auto; 24 | margin-top: 80px; 25 | margin-bottom: 30px; 26 | border-radius: 4px; 27 | background-color: @skeleton-color-darken; 28 | } 29 | .desc { 30 | width: 80%; 31 | height: 20px; 32 | background-color: @skeleton-color-darken; 33 | margin: 10px auto; 34 | &:nth-child(3) { 35 | width: 70%; 36 | } 37 | &:nth-child(4) { 38 | width: 80%; 39 | } 40 | } 41 | } 42 | 43 | .body { 44 | width: 100%; 45 | height: calc(100vh - @header-height); 46 | display: flex; 47 | .left { 48 | flex: 0 0 160px; 49 | width: 160px; 50 | height: 100%; 51 | margin-right: 20px; 52 | background-color: @fill-body-darken; 53 | 54 | .item { 55 | padding: 30px 15px; 56 | border-top: 1px solid @border-color; 57 | &:first-child { 58 | border-top: none; 59 | } 60 | .text { 61 | font-size: @font-size-small; 62 | font-weight: @font-weight-small; 63 | color: @color-base; 64 | min-height: 20px; 65 | width: 80px; 66 | display: block; 67 | background-color: @skeleton-color-darken; 68 | } 69 | } 70 | } 71 | .right { 72 | flex: 1; 73 | } 74 | } 75 | 76 | .footer { 77 | position: absolute; 78 | z-index: 2; 79 | height: @shop-cart-height; 80 | background-color: @skeleton-color-darken; 81 | bottom: 0; 82 | left: 0; 83 | right: 0; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/pages/home/top-bar/index.less: -------------------------------------------------------------------------------- 1 | 2 | @import '../../../assets/css/theme.less'; 3 | @import '../../../assets/css/mixin.less'; 4 | 5 | @search-height: 72px; 6 | 7 | .header { 8 | position: fixed; 9 | z-index: 2; 10 | top: 0; 11 | left: 0; 12 | box-sizing: border-box; 13 | width: 100%; 14 | padding: 0 28px; 15 | background-image: linear-gradient(-90deg, @primary-color, @primary-color-light); 16 | transition: all .3s ease; 17 | .location { 18 | width: 100%; 19 | height: @bar-height; 20 | font-size: 0; 21 | transition: all .3s ease; 22 | .icon { 23 | display: inline-block; 24 | vertical-align: middle; 25 | width: 42px; 26 | height: 100%; 27 | fill: #fff; 28 | margin-right: 6px; 29 | } 30 | .address { 31 | display: inline-block; 32 | vertical-align: middle; 33 | font-size: @font-size-medium-x; 34 | margin-top: 2px; 35 | color: #fff; 36 | max-width: 330px; 37 | margin-right: 16px; 38 | .no-wrap; 39 | } 40 | .down { 41 | display: inline-block; 42 | vertical-align: middle; 43 | width: 16px; 44 | height: 100%; 45 | fill: #fff; 46 | } 47 | } 48 | .search { 49 | width: 100%; 50 | height: @search-height; 51 | line-height: @search-height; 52 | margin-bottom: (@bar-height - @search-height) / 2; 53 | background-color: @fill-body; 54 | font-size: 0; 55 | text-align: center; 56 | transition: all .3s ease; 57 | .icon { 58 | display: inline-block; 59 | vertical-align: middle; 60 | width: 32px; 61 | height: 100%; 62 | fill: @color-base; 63 | margin-right: 6px; 64 | } 65 | .desc { 66 | display: inline-block; 67 | vertical-align: middle; 68 | font-size: @font-size-medium; 69 | color: @color-base; 70 | font-weight: @font-weight-small; 71 | } 72 | } 73 | 74 | &.shrink { 75 | .location { 76 | height: 0; 77 | opacity: 0; 78 | } 79 | .search { 80 | margin-top: (@bar-height - @search-height) / 2; 81 | border-radius: @bar-height; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/components/toast/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import PropTypes from 'prop-types' 4 | import cls from 'classnames' 5 | import Notification from './notification' 6 | import SvgIcon from '../icon-svg' 7 | import styles from './index.less' 8 | 9 | let messageInstance 10 | 11 | const getMessageInstance = (props = {}, callback) => { 12 | if (messageInstance) { 13 | messageInstance.destroy() 14 | messageInstance = null 15 | } 16 | Notification.newInstance(props, (notification) => { 17 | callback && callback(notification) // eslint-disable-line 18 | }) 19 | } 20 | 21 | const notice = (content, duration, mask = true, onClose) => { 22 | getMessageInstance({}, (notification) => { 23 | messageInstance = notification 24 | notification.notice({ 25 | duration, 26 | mask, 27 | content, 28 | onClose: () => { 29 | if (onClose) onClose() 30 | notification.destroy() 31 | messageInstance = null 32 | }, 33 | }) 34 | }) 35 | } 36 | 37 | const WithIcon = ({ name, content, isLoading = false }) => ( 38 |
39 | 42 |

{content}

43 |
44 | ) 45 | 46 | WithIcon.propTypes = { 47 | isLoading: PropTypes.bool, 48 | name: PropTypes.string, 49 | content: PropTypes.string, 50 | }; 51 | 52 | export default { 53 | info: (content, duration, mask = true, onClose) => (notice(content, duration, mask, onClose)), 54 | success: (content, duration, mask = true, onClose) => (notice(, duration, mask, onClose)), 55 | fail: (content, duration, mask = true, onClose) => (notice(, duration, mask, onClose)), 56 | loading: (content, duration, mask = true, onClose) => (notice(, duration, mask, onClose)), 57 | hide: () => { 58 | if (messageInstance) { 59 | messageInstance.destroy() 60 | messageInstance = null 61 | } 62 | }, 63 | } 64 | -------------------------------------------------------------------------------- /src/components/toast/notices.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import cls from 'classnames' 5 | import PropTypes from 'prop-types' 6 | import styles from './index.less' 7 | 8 | const noticeProps = { 9 | duration: PropTypes.number, 10 | content: PropTypes.oneOfType([ 11 | PropTypes.string, 12 | PropTypes.element, 13 | ]), 14 | onClose: PropTypes.func, 15 | } 16 | 17 | const defaultProps = { 18 | duration: 2, 19 | content: '', 20 | onClose: () => {}, 21 | } 22 | 23 | // 动画时间 24 | const animationDuration = 300 25 | const prefixCls = 'notice' 26 | 27 | /** 28 | * Notice 初始化的时候 生成一个定时器 29 | * 根据输入的时间 加载一个动画 然后执行输入的回调 30 | * Notice的显示和隐藏收到父组件Notification的绝对控制 31 | */ 32 | export default class Notice extends React.Component { 33 | static defaultProps = defaultProps 34 | static propTypes = noticeProps 35 | 36 | constructor(props) { 37 | super(props) 38 | // shouldClose 用于判断何时改添加上离场动画 39 | this.state = { 40 | shouldClose: false, 41 | } 42 | } 43 | 44 | componentDidMount() { 45 | if (this.props.duration) { 46 | this.closeTimer = setTimeout(() => { 47 | this.onClose() 48 | }, (this.props.duration * 1000) - animationDuration) 49 | } 50 | } 51 | 52 | componentWillUnmount() { 53 | this.clearCloseTimer() 54 | } 55 | 56 | onClose = () => { 57 | // 清除定时器 并且 开启离场动画 并且等待动画结束后执行 onClose回调 58 | this.clearCloseTimer() 59 | this.setState({ shouldClose: true }) 60 | this.timer = setTimeout(() => { 61 | if (this.props.onClose) { 62 | this.props.onClose() 63 | } 64 | clearTimeout(this.timer) 65 | }, animationDuration) 66 | } 67 | 68 | clearCloseTimer = () => { 69 | if (this.closeTimer) { 70 | clearTimeout(this.closeTimer) 71 | this.closeTimer = null 72 | } 73 | } 74 | 75 | render() { 76 | const { shouldClose } = this.state 77 | const { content } = this.props 78 | 79 | return ( 80 |
84 |
{content}
85 |
86 | ) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /static/css/reset.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/) 3 | * http://cssreset.com 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 | menu, 77 | nav, 78 | output, 79 | ruby, 80 | section, 81 | summary, 82 | time, 83 | mark, 84 | audio, 85 | video, 86 | input { 87 | margin: 0; 88 | padding: 0; 89 | border: 0; 90 | font-size: 100%; 91 | font-weight: normal; 92 | vertical-align: baseline; 93 | } 94 | 95 | 96 | /* HTML5 display-role reset for older browsers */ 97 | 98 | article, 99 | aside, 100 | details, 101 | figcaption, 102 | figure, 103 | footer, 104 | header, 105 | menu, 106 | nav, 107 | section { 108 | display: block; 109 | } 110 | 111 | body { 112 | line-height: 1; 113 | } 114 | 115 | blockquote, 116 | q { 117 | quotes: none; 118 | } 119 | 120 | blockquote:before, 121 | blockquote:after, 122 | q:before, 123 | q:after { 124 | content: none; 125 | } 126 | 127 | table { 128 | border-collapse: collapse; 129 | border-spacing: 0; 130 | } 131 | 132 | 133 | /* custom */ 134 | 135 | a { 136 | color: #818181; 137 | text-decoration: none; 138 | -webkit-backface-visibility: hidden; 139 | } 140 | 141 | html, 142 | body { 143 | width: 100%; 144 | color: #818181; 145 | background-color: #f4f4f4; 146 | font-weight: 200; 147 | font-family: 'PingFang SC', 'STHeitiSC-Light', 'Arial, Helvetica, sans-serif'; 148 | } 149 | -------------------------------------------------------------------------------- /src/pages/shop-detail/shop/index.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | @import '../../../assets/css/theme.less'; 4 | @import '../../../assets/css/mixin.less'; 5 | 6 | .shop-info { 7 | width: 100%; 8 | height: 100%; 9 | box-sizing: border-box; 10 | padding-bottom: @shop-cart-height; 11 | background-color: @fill-body-darken; 12 | position: relative; 13 | z-index: 1; 14 | .scroll { 15 | background-color: @fill-body-darken; 16 | bottom: @shop-cart-height; 17 | } 18 | 19 | .card { 20 | background-color: @fill-body; 21 | margin-bottom: 20px; 22 | padding: 30px; 23 | .title { 24 | font-weight: bold; 25 | color: #000; 26 | font-size: @font-size-medium; 27 | margin-bottom: 20px; 28 | } 29 | .desc { 30 | font-size: @font-size-small; 31 | font-weight: @font-weight-small; 32 | color: @color-base; 33 | line-height: 40px; 34 | } 35 | 36 | .img { 37 | display: inline-block; 38 | vertical-align: middle; 39 | margin-right: 10px; 40 | margin-top: 10px; 41 | width: 140px; 42 | height: 140px; 43 | img { 44 | width: 100%; 45 | height: 100%; 46 | } 47 | } 48 | 49 | .activities { 50 | display: flex; 51 | align-items: center; 52 | margin-bottom: 10px; 53 | &:last-child { 54 | margin-bottom: 0; 55 | } 56 | .icon { 57 | flex: 0 0 60px; 58 | width: 60px; 59 | height: 30px; 60 | color: #fff; 61 | position: relative; 62 | } 63 | .tips { 64 | flex: 1; 65 | margin-left: 10px; 66 | line-height: 30px; 67 | font-size: @font-size-small; 68 | font-weight: @font-weight-small; 69 | color: @color-base; 70 | } 71 | } 72 | 73 | .item { 74 | display: flex; 75 | border-bottom: 1px solid @border-color; 76 | padding: 30px 0; 77 | justify-content: space-between; 78 | .label { 79 | flex: 0 0 110px; 80 | width: 110px; 81 | font-size: @font-size-small; 82 | color: #000; 83 | } 84 | .value { 85 | flex: 1; 86 | font-size: @font-size-small; 87 | font-weight: @font-weight-small; 88 | color: @color-base; 89 | text-align: right; 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/pages/search-shop/index.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | @import '../../assets/css/theme.less'; 4 | @import '../../assets/css/mixin.less'; 5 | 6 | .search { 7 | position: fixed; 8 | .position-full; 9 | background-color: @fill-body; 10 | 11 | .search-bar { 12 | height: 88px; 13 | box-sizing: border-box; 14 | padding: 20px 30px 4px 30px; 15 | display: flex; 16 | align-items: center; 17 | .input { 18 | flex: 1; 19 | box-sizing: border-box; 20 | border-radius: 4px; 21 | overflow: hidden; 22 | background-color: #f5f5f5; 23 | border: 1px solid @border-color; 24 | height: 58px; 25 | line-height: 58px; 26 | box-sizing: border-box; 27 | padding: 0 20px 0 60px; 28 | position: relative; 29 | .icon { 30 | position: absolute; 31 | left: 14px; 32 | top: 50%; 33 | transform: translateY(-50%); 34 | font-size: @font-size-medium; 35 | color: @color-base; 36 | } 37 | input { 38 | display: block; 39 | background-color: transparent; 40 | font-size: @font-size-small; 41 | font-weight: @font-weight-small; 42 | color: @color-base; 43 | outline: none; 44 | width: 100%; 45 | line-height: 58px; 46 | } 47 | } 48 | .btn { 49 | flex: 0 0 120px; 50 | width: 120px; 51 | height: 58px; 52 | margin-left: 20px; 53 | background-color: @primary-color-light; 54 | color: #fff; 55 | font-size: @font-size-medium; 56 | line-height: 58px; 57 | border-radius: 4px; 58 | outline: none; 59 | border: none; 60 | } 61 | } 62 | 63 | .hot { 64 | padding: 30px; 65 | box-sizing: border-box; 66 | width: 100%; 67 | .title { 68 | font-size: @font-size-medium; 69 | color: @color-title; 70 | } 71 | .badge { 72 | display: inline-block; 73 | padding: 15px 20px; 74 | background-color: @fill-body-darken; 75 | font-size: @font-size-medium; 76 | font-weight: @font-weight-small; 77 | line-height: 40px; 78 | color: @color-base; 79 | border-radius: 4px; 80 | margin-top: 20px; 81 | margin-right: 20px; 82 | } 83 | } 84 | } 85 | 86 | .shops { 87 | position: fixed; 88 | top: @bar-height + 88px; 89 | left: 0; 90 | right: 0; 91 | bottom: 0; 92 | background-color: @fill-body; 93 | } 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## React版高仿饿了么webApp 2 | ## 说明 3 | > 此项目部分数据接口需要手机登陆后才能访问,我是在mac上开发的,并且只在chrome和safari以及自己的iphone上简单浏览测试了下,如果有什么问题欢迎提出😋,启动后webpack的warning是因为使用的postcss-viewport-units适配vw,wh,vmin,vmax后会在css的content的打上viewport-units-buggyfill, 所以当我用!important修改content时postcss-viewport-units会给出警告,如果看着不爽可以在node_modules/postcss-viewport-units/index.js中注释掉warning就行了 4 | 5 | ## 技术栈 6 | >react, react-router4, redux, redux-thunk, betterScroll, webpack4 7 | 8 | ##项目运行 9 | >首先clone数据接口,请确保3333端口没被占用或自行更改app/app.js中的端口号,🙈因为重点是前端(其实是我不会后端😂)所以只是用express和axios转发了请求,代码比较粗糙,也没有使用babel,所以请确保使用高版本node😋 10 | 11 | ``` 12 | $ git clone https://github.com/gaojingran/eleme-api.git 13 | $ npm install 14 | $ npm start 15 | ``` 16 | >前端项目 17 | 18 | ``` 19 | $ git clone https://github.com/gaojingran/react-eleme.git 20 | $ npm install 21 | $ npm start 22 | ``` 23 | 24 | ## 效果演示 25 | [查看demo请戳这里](http://elm.superoreo.cn)(请用chrome手机模式预览)) 26 | 27 | 28 | 29 | ## 项目截图 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/assets/svg/cry.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/svg/cart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/rate/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import PropTypes from 'prop-types' 5 | import cls from 'classnames'; 6 | import { prefixStyle } from 'utils/dom' 7 | import styles from './index.less' 8 | 9 | /** 10 | * color 颜色 11 | * value 数值 12 | * length 星星颗数 13 | * size 星星大小 14 | * animate 动画时间 15 | * readonly 是否只读 16 | */ 17 | const rateProps = { 18 | className: PropTypes.string, 19 | color: PropTypes.string, 20 | value: PropTypes.number, 21 | length: PropTypes.number, 22 | animate: PropTypes.number, 23 | size: PropTypes.string, 24 | // readonly: PropTypes.bool, 25 | } 26 | 27 | const defaultValue = { 28 | className: '', 29 | color: '#fed100', 30 | value: 0, 31 | length: 5, 32 | size: '1em', 33 | animate: 0, 34 | // readonly: true, 35 | } 36 | 37 | const transition = prefixStyle('transition') 38 | 39 | export default class Rate extends React.Component { 40 | static defaultProps = defaultValue 41 | static propTypes = rateProps 42 | 43 | constructor(props) { 44 | super(props) 45 | this.state = { 46 | stars: new Array(props.length - 0).fill('★'), 47 | hollow: new Array(props.length - 0).fill('☆'), 48 | styleObject: {}, 49 | } 50 | this.styleFont = { 51 | color: props.color, 52 | fontSize: props.size, 53 | } 54 | } 55 | 56 | componentDidMount() { 57 | if (!this.props.animate) { 58 | this.setStyle() 59 | } 60 | this.timer = setTimeout(() => this.setStyle(), 60) 61 | } 62 | 63 | componentWillUnmount() { 64 | if (this.timer) { 65 | clearTimeout(this.timer) 66 | this.timer = null 67 | } 68 | } 69 | 70 | setStyle = () => { 71 | this.setState(() => { 72 | return { 73 | styleObject: { 74 | width: `${this.props.value}em`, 75 | [transition]: `width ${this.props.animate}s`, 76 | }, 77 | } 78 | }) 79 | } 80 | 81 | render() { 82 | const { stars, hollow, styleObject } = this.state 83 | const { className } = this.props 84 | return ( 85 |
86 | { 87 | hollow.map((v, i) => ( 88 | {v} 89 | )) 90 | } 91 |
92 | { 93 | stars.map((v, i) => ( 94 | {v} 95 | )) 96 | } 97 |
98 |
99 | ) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/pages/benefit/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import Scroll from 'components/scroll' 5 | import { connect } from 'react-redux' 6 | import Toast from 'components/toast' 7 | import Loading from 'components/loading' 8 | import NavBar from '../common-components/nav-bar' 9 | import AuthError from '../common-components/auth-err' 10 | import NoData from '../common-components/no-data' 11 | import RedTicketRow from '../common-components/red-ticket-row' 12 | import { getHongbaos } from '../../api' 13 | import styles from './index.less' 14 | 15 | @connect(({ globalState }) => ({ 16 | isLogin: globalState.isLogin, 17 | })) 18 | export default class Benefit extends React.Component { 19 | constructor(props) { 20 | super(props) 21 | this.state = { 22 | list: [], 23 | init: true, 24 | hasMore: true, 25 | } 26 | props.isLogin && this.getList(true) // eslint-disable-line 27 | } 28 | 29 | componentWillReceiveProps(nextProps) { 30 | if (nextProps.isLogin && nextProps.isLogin !== this.props.isLogin) { 31 | this.getList(true) 32 | } 33 | } 34 | 35 | getList = async (isRefresh = false) => { 36 | const { list } = this.state 37 | let nextPayload = {} 38 | try { 39 | const { data } = await getHongbaos({ 40 | limit: 20, 41 | offset: isRefresh ? 0 : list.length, 42 | }) 43 | nextPayload = { 44 | list: [...list, ...data], 45 | hasMore: data.length === 20, 46 | } 47 | } catch ({ err }) { 48 | Toast.info(err) 49 | } 50 | this.setState({ 51 | ...nextPayload, 52 | init: false, 53 | }) 54 | } 55 | 56 | render() { 57 | const { isLogin, history } = this.props 58 | const { list, init, hasMore } = this.state 59 | const scrollProps = { 60 | className: styles.scroll, 61 | dataSource: list, 62 | pullDownRefresh: { stop: 40 }, 63 | pullUpLoad: hasMore, 64 | pullingDown: () => this.getList(true), 65 | pullingUp: () => this.getList(), 66 | } 67 | 68 | return !isLogin ? : ( 69 |
70 | this.props.history.goBack()} /> 74 | { 75 | init ? : list.length ? ( 76 | 77 | { 78 | list.map(v => ( 79 | history.push('/home')} /> 80 | )) 81 | } 82 | 83 | ) : 84 | } 85 |
86 | ) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/pages/address/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import { connect } from 'react-redux' 5 | import Toast from 'components/toast' 6 | import SvgIcon from 'components/icon-svg' 7 | import Scroll from 'components/scroll' 8 | import Loading from 'components/loading' 9 | import NavBar from '../common-components/nav-bar' 10 | import NoData from '../common-components/no-data' 11 | import AuthError from '../common-components/auth-err' 12 | import AddressRow from './address-row' 13 | import { getAddress } from '../../api' 14 | import styles from './index.less' 15 | 16 | @connect(({ globalState }) => ({ 17 | isLogin: globalState.isLogin, 18 | })) 19 | export default class Address extends React.Component { 20 | constructor(props) { 21 | super(props) 22 | this.state = { 23 | list: [], 24 | loading: false, 25 | } 26 | } 27 | 28 | componentDidMount() { 29 | this.props.isLogin && this.getAddress() // eslint-disable-line 30 | } 31 | 32 | componentWillReceiveProps(nextProps) { 33 | if (nextProps.isLogin && !this.state.loading) { 34 | this.getAddress() 35 | } 36 | } 37 | 38 | getAddress = async () => { 39 | try { 40 | this.setState({ loading: true }) 41 | const { data } = await getAddress() 42 | this.setState({ 43 | list: data, 44 | loading: false, 45 | }) 46 | } catch ({ err }) { 47 | this.setState({ loading: false }) 48 | Toast.info(err) 49 | } 50 | } 51 | 52 | goEdit = (val = false) => { 53 | this.props.history.push({ 54 | pathname: '/address-edit', 55 | state: val, 56 | }) 57 | } 58 | 59 | render() { 60 | const { isLogin } = this.props 61 | const { list, loading } = this.state 62 | 63 | return !isLogin ? : ( 64 |
65 | this.props.history.goBack()} /> 69 | { 70 | loading ? : list.length ? ( 71 |
72 | 73 | { 74 | list.map(v => ( 75 | this.goEdit(v)} /> 76 | )) 77 | } 78 | 79 |
80 | ) : 81 | } 82 | 88 |
89 | ) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/stores/shop.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | import Toast from 'components/toast' 4 | import { getShopInfo, getShopRatings, getShopFood, getRatingTags, getRatingScores } from '../api' 5 | 6 | const UPDATE = 'SHOP_UPDATE' 7 | 8 | const initState = { 9 | loading: true, 10 | info: {}, 11 | menu: [], 12 | tags: [], 13 | tagIndex: '', 14 | foodMenuIndex: 0, 15 | ratings: [], 16 | restaurant_id: null, 17 | } 18 | 19 | export const shop = (state = initState, action) => { 20 | switch (action.type) { 21 | case UPDATE: 22 | return { 23 | ...state, 24 | ...action.payload, 25 | } 26 | default: 27 | return state 28 | } 29 | } 30 | 31 | export const shopUpdate = (params) => { 32 | return { 33 | payload: params, 34 | type: UPDATE, 35 | } 36 | } 37 | 38 | export const shopDestroy = () => { 39 | return { 40 | payload: initState, 41 | type: UPDATE, 42 | } 43 | } 44 | 45 | export const shopInit = (params) => { 46 | return async (dispatch) => { 47 | const { restaurant_id } = params 48 | try { 49 | const [info, menu, ratings, tags, scores] = await Promise.all([ 50 | getShopInfo({ 51 | ...params, 52 | terminal: 'h5', 53 | extras: ['activities', 'albums', 'license', 'identification', 'qualification'], 54 | }), 55 | getShopFood({ restaurant_id }), 56 | getShopRatings({ 57 | restaurant_id, 58 | has_content: true, 59 | offset: 0, 60 | limit: 8, 61 | }), 62 | getRatingTags({ restaurant_id }), 63 | getRatingScores({ restaurant_id }), 64 | ]) 65 | dispatch(shopUpdate({ 66 | restaurant_id, 67 | loading: false, 68 | info: info.data, 69 | menu: menu.data, 70 | ratings: ratings.data, 71 | tags: tags.data, 72 | tagIndex: tags.data.length ? tags.data[0].name : '', 73 | scores: scores.data, 74 | })) 75 | } catch ({ err }) { 76 | Toast.info(err, 3, false) 77 | } 78 | } 79 | } 80 | 81 | export const changeRatingTag = (params) => { 82 | return async (dispatch, getState) => { 83 | const { restaurant_id } = getState().shop 84 | Toast.loading('加载中...', 0) 85 | try { 86 | const { data } = await getShopRatings({ 87 | restaurant_id, 88 | has_content: true, 89 | tag_name: params, 90 | offset: 0, 91 | limit: 8, 92 | }) 93 | dispatch(shopUpdate({ 94 | ratings: data, 95 | tagIndex: params, 96 | })) 97 | setTimeout(() => Toast.hide(), 400) 98 | } catch ({ err }) { 99 | Toast.info(err, 3, false) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/pages/common-components/skeleton/index.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | @import '../../../assets/css/mixin.less'; 4 | @import '../../../assets/css/theme.less'; 5 | 6 | .entry { 7 | display: flex; 8 | width: 100%; 9 | flex-wrap: wrap; 10 | .item { 11 | width: 20vw; 12 | margin-top: 22px; 13 | .flex-center; 14 | flex-direction: column; 15 | .circle { 16 | width: 90px; 17 | height: 90px; 18 | border-radius: 100%; 19 | background-color: @skeleton-color; 20 | } 21 | .desc { 22 | width: 50px; 23 | height: 20px; 24 | margin-top: 10px; 25 | background-color: @skeleton-color; 26 | } 27 | } 28 | } 29 | 30 | 31 | .row { 32 | display: flex; 33 | padding: 30px 20px; 34 | .left { 35 | flex: 0 0 130px; 36 | width: 130px; 37 | height: 130px; 38 | background-color: @skeleton-color; 39 | margin-right: 20px; 40 | } 41 | .right { 42 | flex: 1; 43 | .desc { 44 | height: 20px; 45 | background-image: linear-gradient( 46 | to right, 47 | @skeleton-color 8%, 48 | #eee 18%, 49 | @skeleton-color 33% 50 | ); 51 | margin: 10px 0; 52 | animation: progress 2s linear infinite; 53 | } 54 | .desc:nth-child(1) { 55 | width: 90%; 56 | } 57 | .desc:nth-child(2) { 58 | width: 90%; 59 | } 60 | .desc:nth-child(3) { 61 | width: 80% 62 | } 63 | .desc:nth-child(4) { 64 | width: 70% 65 | } 66 | 67 | } 68 | } 69 | 70 | .recommend { 71 | display: inline-block; 72 | border: 1px solid @border-color; 73 | margin: 1vw; 74 | width: 46vw; 75 | background-color: @fill-body; 76 | transform: translate3d(0,0,0); 77 | .pic { 78 | width: 100%; 79 | height: 46vw; 80 | background-color: @skeleton-color; 81 | } 82 | .desc { 83 | width: 100%; 84 | box-sizing: border-box; 85 | padding: 8px 16px 8px 20px; 86 | .placeholder { 87 | width: 100%; 88 | height: 20px; 89 | background-image: linear-gradient( 90 | to right, 91 | @skeleton-color 8%, 92 | #eee 18%, 93 | @skeleton-color 33% 94 | ); 95 | margin: 10px 0; 96 | animation: progress 3s linear infinite; 97 | } 98 | .placeholder:nth-child(2) { 99 | width: 90%; 100 | } 101 | .placeholder:nth-child(3) { 102 | width: 80%; 103 | } 104 | } 105 | } 106 | 107 | @keyframes progress{ 108 | from { background-position: -400px 0 } 109 | to { background-position: 400px 0 } 110 | } 111 | -------------------------------------------------------------------------------- /src/stores/search-shop.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | import Toast from 'components/toast' 4 | import { getHotKeywords, getGeolocation, getShopListByKw } from '../api' 5 | import { homeUpdate } from './home' 6 | 7 | const UPDATE = 'SEARCHSHOP_UPDATE' 8 | 9 | const initState = { 10 | loading: false, 11 | keywords: '', 12 | shopLists: [], 13 | hotKeys: [], 14 | rank_id: undefined, 15 | } 16 | 17 | export const searchShop = (state = initState, action) => { 18 | switch (action.type) { 19 | case UPDATE: 20 | return { 21 | ...state, 22 | ...action.payload, 23 | } 24 | default: 25 | return state 26 | } 27 | } 28 | 29 | export const searchShopUpdate = (params) => { 30 | return { 31 | payload: params, 32 | type: UPDATE, 33 | } 34 | } 35 | 36 | export const searchShopDestroy = () => { 37 | return { 38 | payload: initState, 39 | type: UPDATE, 40 | } 41 | } 42 | 43 | export const getHotKeys = () => { 44 | return async (dispatch, getState) => { 45 | let { locationInfo } = getState().home 46 | if (!locationInfo.latitude && !locationInfo.longitude) { 47 | try { 48 | const { data } = await getGeolocation() 49 | if (data) { 50 | locationInfo = data 51 | dispatch(homeUpdate({ 52 | locationInfo: data, 53 | })) 54 | } 55 | } catch ({ err }) { 56 | return Toast.info(err) 57 | } 58 | } 59 | try { 60 | const { data } = await getHotKeywords({ 61 | latitude: locationInfo.latitude, 62 | longitude: locationInfo.longitude, 63 | }) 64 | dispatch(searchShopUpdate({ 65 | hotKeys: data, 66 | })) 67 | } catch ({ err }) { 68 | Toast.info(err) 69 | } 70 | } 71 | } 72 | 73 | export const getShopList = () => { 74 | return async (dispatch, getState) => { 75 | const { locationInfo } = getState().home 76 | const { keywords } = getState().searchShop 77 | dispatch(searchShopUpdate({ 78 | loading: true, 79 | shopLists: [], 80 | })) 81 | let nextPayload = {} 82 | try { 83 | const { data } = await getShopListByKw({ 84 | keyword: keywords, 85 | latitude: locationInfo.latitude, 86 | longitude: locationInfo.longitude, 87 | offset: 0, 88 | limit: 15, 89 | search_item_type: 0, 90 | is_rewrite: 1, 91 | extras: ['activities'], 92 | terminal: 'h5', 93 | }) 94 | nextPayload = { 95 | shopLists: data, 96 | } 97 | } catch ({ err }) { 98 | Toast.info(err) 99 | } 100 | dispatch(searchShopUpdate({ 101 | ...nextPayload, 102 | loading: false, 103 | })) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-eleme", 3 | "version": "1.0.0", 4 | "description": "饿了么webapp", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.js", 8 | "clear": "rimraf dist", 9 | "precommit": "eslint --ext .js --ext .jsx src/", 10 | "build": "npm run clear && webpack --config build/webpack.config.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/gaojingran/react-eleme.git" 15 | }, 16 | "keywords": [ 17 | "react", 18 | "redux", 19 | "betterscroll", 20 | "webpack" 21 | ], 22 | "author": "gaojingran", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/gaojingran/react-eleme/issues" 26 | }, 27 | "homepage": "https://github.com/gaojingran/react-eleme#readme", 28 | "devDependencies": { 29 | "autoprefixer": "^8.2.0", 30 | "babel-core": "^6.26.0", 31 | "babel-eslint": "^8.2.3", 32 | "babel-loader": "^7.1.4", 33 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 34 | "babel-plugin-transform-runtime": "^6.23.0", 35 | "babel-preset-env": "^1.6.1", 36 | "babel-preset-react": "^6.24.1", 37 | "babel-preset-stage-0": "^6.24.1", 38 | "babel-runtime": "^6.26.0", 39 | "copy-webpack-plugin": "^4.5.1", 40 | "cross-env": "^5.1.4", 41 | "css-loader": "^0.28.11", 42 | "eslint": "^4.19.1", 43 | "eslint-config-airbnb": "^16.1.0", 44 | "eslint-loader": "^2.0.0", 45 | "eslint-plugin-import": "^2.11.0", 46 | "eslint-plugin-jsx-a11y": "^6.0.3", 47 | "eslint-plugin-react": "^7.7.0", 48 | "file-loader": "^1.1.11", 49 | "happypack": "^5.0.0", 50 | "html-webpack-plugin": "^3.2.0", 51 | "husky": "^0.14.3", 52 | "less": "^3.0.1", 53 | "less-loader": "^4.1.0", 54 | "mini-css-extract-plugin": "^0.4.0", 55 | "postcss-loader": "^2.1.3", 56 | "postcss-px-to-viewport": "0.0.3", 57 | "postcss-viewport-units": "^0.1.4", 58 | "react-hot-loader": "^4.0.1", 59 | "rimraf": "^2.6.2", 60 | "style-loader": "^0.20.3", 61 | "svg-sprite-loader": "^3.7.3", 62 | "webpack": "^4.5.0", 63 | "webpack-cli": "^2.0.14", 64 | "webpack-dev-server": "^3.1.3", 65 | "webpack-merge": "^4.1.2" 66 | }, 67 | "dependencies": { 68 | "axios": "^0.18.0", 69 | "better-scroll": "^1.10.2", 70 | "classnames": "^2.2.5", 71 | "lodash.omit": "^4.5.0", 72 | "numeral": "^2.0.6", 73 | "prop-types": "^15.6.1", 74 | "query-string": "^6.0.0", 75 | "rc-queue-anim": "^1.5.0", 76 | "react": "^16.3.1", 77 | "react-dom": "^16.3.1", 78 | "react-redux": "^5.0.7", 79 | "react-router-dom": "^4.2.2", 80 | "react-transition-group": "^2.3.1", 81 | "redux": "^3.7.2", 82 | "redux-thunk": "^2.2.0" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/pages/common-components/recommend-food-row/index.less: -------------------------------------------------------------------------------- 1 | 2 | @import '../../../assets/css/mixin.less'; 3 | @import '../../../assets/css/theme.less'; 4 | 5 | .row { 6 | display: inline-block; 7 | border: 1px solid @border-color; 8 | margin: 1vw; 9 | width: 46vw; 10 | background-color: @fill-body; 11 | transform: translate3d(0,0,0); 12 | .pic { 13 | width: 100%; 14 | height: 46vw; 15 | position: relative; 16 | background-color: @fill-body-darken; 17 | img { 18 | width: 100%; 19 | height: 46vw; 20 | content: none !important; 21 | } 22 | .tip { 23 | position: absolute; 24 | left: 0; 25 | bottom: 0; 26 | width: 100%; 27 | height: 40px; 28 | background-color: rgba(0,0,0,.7); 29 | span { 30 | display: inline-block; 31 | line-height: 40px; 32 | transform: scale(.8); 33 | transform-origin: center; 34 | font-size: @font-size-small; 35 | font-weight: @font-weight-small; 36 | color: #fff; 37 | } 38 | } 39 | } 40 | 41 | .desc { 42 | width: 100%; 43 | box-sizing: border-box; 44 | padding: 8px 16px 8px 20px; 45 | background-color: @fill-body-darken; 46 | .title { 47 | .no-wrap; 48 | font-size: @font-size-medium; 49 | color: @color-title; 50 | text-align: left; 51 | line-height: 40px; 52 | margin: 4px 0; 53 | } 54 | .sell { 55 | .no-wrap; 56 | font-size: @font-size-small; 57 | font-weight: @font-weight-small; 58 | color: @color-base; 59 | text-align: left; 60 | line-height: 30px; 61 | transform: scale(.8); 62 | transform-origin: 0 0; 63 | } 64 | .amount { 65 | font-size: 0; 66 | text-align: left; 67 | margin: 4px 0 10px 0; 68 | span { 69 | display: inline-block; 70 | vertical-align: middle; 71 | font-size: @font-size-medium; 72 | color: #ff6000; 73 | } 74 | .unit { 75 | font-size: @font-size-small; 76 | font-weight: @font-weight-small; 77 | margin-right: 4px; 78 | } 79 | } 80 | 81 | .shop { 82 | font-size: 0; 83 | text-align: left; 84 | height: 40px; 85 | line-height: 40px; 86 | border-top: 1px dashed @border-color; 87 | padding-top: 4px; 88 | .icon, .name{ 89 | display: inline-block; 90 | vertical-align: middle; 91 | font-size: @font-size-small; 92 | font-weight: @font-weight-small; 93 | color: @color-base; 94 | } 95 | .icon { 96 | font-size: @font-size-large; 97 | } 98 | .name { 99 | margin-left: 10px; 100 | width: 260px; 101 | .no-wrap; 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import { connect } from 'react-redux' 5 | import { bindActionCreators } from 'redux' 6 | import { Switch, Route, Redirect } from 'react-router-dom' 7 | import asyncLoad from 'components/async-loade' 8 | import Loading from './common-components/lazy-loading' 9 | import { getUserInfo } from '../api' 10 | import { globalUpdate } from '../stores/global' 11 | 12 | @connect(() => ({}), dispatch => bindActionCreators({ 13 | globalUpdate, 14 | }, dispatch)) 15 | class AuthComponent extends React.Component { 16 | async componentDidMount() { 17 | try { 18 | const { data } = await getUserInfo() 19 | this.props.globalUpdate({ 20 | isLogin: true, 21 | userInfo: data, 22 | }) 23 | } catch (err) { 24 | this.props.globalUpdate({ 25 | globalUpdate: false, 26 | userInfo: {}, 27 | }) 28 | } 29 | } 30 | 31 | render() { 32 | return null 33 | } 34 | } 35 | 36 | const home = asyncLoad(() => import('./home'), ) 37 | const order = asyncLoad(() => import('./order'), ) 38 | const orderDetail = asyncLoad(() => import('./order-detail'), ) 39 | const compass = asyncLoad(() => import('./compass'), ) 40 | const profile = asyncLoad(() => import('./profile'), ) 41 | const login = asyncLoad(() => import('./login'), ) 42 | const restaurant = asyncLoad(() => import('./restaurant'), ) 43 | const shopDetail = asyncLoad(() => import('./shop-detail'), ) 44 | const address = asyncLoad(() => import('./address'), ) 45 | const addressEdit = asyncLoad(() => import('./address-edit'), ) 46 | const searchAddress = asyncLoad(() => import('./address-nearby'), ) 47 | const searchShop = asyncLoad(() => import('./search-shop'), ) 48 | const benefit = asyncLoad(() => import('./benefit'), ) 49 | 50 | export default () => ( 51 | 52 | 53 | 54 | } /> 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | ) 71 | 72 | -------------------------------------------------------------------------------- /src/pages/address-edit/index.less: -------------------------------------------------------------------------------- 1 | 2 | @import '../../assets/css/theme.less'; 3 | @import '../../assets/css/mixin.less'; 4 | 5 | .badge { 6 | display: inline-block; 7 | width: 126px; 8 | height: 54px; 9 | line-height: 54px; 10 | background-color: @fill-body; 11 | border: 1px solid @border-color; 12 | text-align: center; 13 | font-size: @font-size-medium; 14 | font-weight: @font-weight-small; 15 | color: @color-title; 16 | margin-right: 10px; 17 | border-radius: 6px; 18 | &.active { 19 | border-color: @primary-color-light; 20 | color: @primary-color-light; 21 | background-color: #eef7ff; 22 | } 23 | } 24 | 25 | .address { 26 | width: 100%; 27 | height: 100%; 28 | background-color: @fill-body-darken; 29 | position: relative; 30 | z-index: 1; 31 | 32 | .form { 33 | width: 100%; 34 | padding-left: 30px; 35 | background-color: @fill-body; 36 | box-sizing: border-box; 37 | .item { 38 | display: flex; 39 | position: relative; 40 | align-items: center; 41 | .line { 42 | width: 100%; 43 | position: absolute; 44 | left: 0; 45 | right: 0; 46 | bottom: 0; 47 | height: 1px; 48 | } 49 | .label { 50 | align-self: flex-start; 51 | flex: 0 0 140px; 52 | width: 140px; 53 | line-height: 38px; 54 | padding: 28px 0; 55 | font-size: @font-size-medium; 56 | color: @color-title; 57 | } 58 | .control { 59 | flex: 1; 60 | width: 0; 61 | position: relative; 62 | .line { 63 | position: static; 64 | height: 1px; 65 | } 66 | .placeholder-color(#c4c4c4); 67 | .input, .textarea { 68 | margin: 28px 0; 69 | width: 100%; 70 | font-size: @font-size-medium; 71 | font-weight: @font-weight-small; 72 | color: @color-base; 73 | line-height: 38px; 74 | outline: none; 75 | } 76 | .textarea { 77 | padding: 0; 78 | margin: 28px 0; 79 | resize: none; 80 | border: none; 81 | } 82 | .tag-wrapper { 83 | padding: 28px 0; 84 | } 85 | } 86 | 87 | .icon { 88 | margin: 0 20px; 89 | flex: 0 0 40px; 90 | width: 40px; 91 | font-size: @font-size-large-x; 92 | color: #999; 93 | } 94 | } 95 | } 96 | 97 | .btn { 98 | margin: 0 auto; 99 | display: block; 100 | background-color: #4cd96f; 101 | border-radius: 8px; 102 | outline: none; 103 | border: none; 104 | width: 90%; 105 | height: 84px; 106 | line-height: 84px; 107 | margin-top: 30px; 108 | font-size: @font-size-medium-x; 109 | font-weight: @font-weight-small; 110 | color:#fff; 111 | } 112 | } 113 | 114 | -------------------------------------------------------------------------------- /src/pages/restaurant/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import { connect } from 'react-redux' 5 | import { bindActionCreators } from 'redux' 6 | import Loading from 'components/loading' 7 | import Scroll from 'components/scroll' 8 | import { restaurantDestroy, restaurantInit, fetchShopList } from '../../stores/restaurant' 9 | import ShopListRow from '../common-components/shop-list-row' 10 | import NavBar from '../common-components/nav-bar' 11 | import NoData from '../common-components/no-data' 12 | import SiftFactors from './sift-factors' 13 | import FilterBar from './filter' 14 | import styles from './index.less' 15 | 16 | const mapStateToProp = ({ restaurant, home }) => ({ 17 | loading: restaurant.loading, 18 | shopList: restaurant.shopList, 19 | locationInfo: home.locationInfo, 20 | }) 21 | const mapDispatchToProps = dispatch => bindActionCreators({ 22 | restaurantInit, 23 | restaurantDestroy, 24 | fetchShopList, 25 | }, dispatch) 26 | 27 | @connect(mapStateToProp, mapDispatchToProps) 28 | export default class Shop extends React.Component { 29 | componentDidMount() { 30 | const { location } = this.props 31 | this.props.restaurantInit(location.state) 32 | } 33 | 34 | componentWillUnmount() { 35 | this.props.restaurantDestroy() 36 | } 37 | 38 | refreshScroll = () => { 39 | this.scroll && this.scroll.refresh() // eslint-disable-line 40 | } 41 | 42 | handleRowClick = (val) => { 43 | const { history, locationInfo } = this.props 44 | const { id } = val 45 | const { latitude, longitude } = locationInfo 46 | history.push({ 47 | pathname: '/shop-detail', 48 | search: `?restaurant_id=${id}&latitude=${latitude}&longitude=${longitude}`, 49 | }) 50 | } 51 | 52 | render() { 53 | const { location, loading, shopList } = this.props 54 | 55 | const renderList = () => { 56 | if (loading && !shopList.length) { 57 | return 58 | } 59 | return ( 60 | this.props.fetchShopList({}, true)} 64 | ref={c => this.scroll = c}> 65 | { 66 | shopList.length ? shopList.map((v, i) => ( 67 | this.handleRowClick(v)} /> 72 | )) : 73 | } 74 | 75 | ) 76 | } 77 | 78 | return ( 79 |
80 | this.props.history.goBack()} /> 84 | 85 | 86 |
87 | {renderList()} 88 |
89 |
90 | ) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/stores/home.js: -------------------------------------------------------------------------------- 1 | 2 | import omit from 'lodash.omit' 3 | import Toast from 'components/toast' 4 | import { getGeolocation, getEntry, getBanner, getShopList } from '../api' 5 | 6 | const UPDATE = 'HOME_UPDATE' 7 | 8 | const initState = { 9 | init: false, 10 | topBarShrink: false, 11 | locationInfo: {}, 12 | banner: [], 13 | entry: [], 14 | shoplist: [], 15 | rank_id: undefined, 16 | } 17 | 18 | export const home = (state = initState, action) => { 19 | switch (action.type) { 20 | case UPDATE: 21 | return { 22 | ...state, 23 | ...action.payload, 24 | } 25 | default: 26 | return state 27 | } 28 | } 29 | 30 | export const homeUpdate = (params) => { 31 | return { 32 | payload: params, 33 | type: UPDATE, 34 | } 35 | } 36 | 37 | export const homeInit = () => { 38 | return async (dispatch, getState) => { 39 | const { init } = getState().home 40 | let { locationInfo } = getState().home 41 | if (init) return 42 | try { 43 | // 定理位置 44 | if (!locationInfo.latitude && !locationInfo.longitude) { 45 | const geoInfo = await getGeolocation() 46 | dispatch(homeUpdate({ locationInfo: geoInfo.data })) 47 | locationInfo = geoInfo.data // eslint-disable-line 48 | } 49 | const location = { ...omit(locationInfo, ['address']) } 50 | // 获取banner entry 51 | const [banner, entry, list] = await Promise.all([ 52 | getBanner(location), 53 | getEntry(location), 54 | getShopList({ 55 | ...location, 56 | terminal: 'h5', 57 | offset: 0, 58 | limit: 8, 59 | extra_filters: 'home', 60 | extras: ['activities', 'tags'], 61 | rank_id: '', 62 | }), 63 | ]) 64 | dispatch(homeUpdate({ 65 | banner: banner.data, 66 | entry: entry.data, 67 | shoplist: list.data.items, 68 | rank_id: list.data.meta.rank_id, 69 | init: true, 70 | })) 71 | } catch ({ err }) { 72 | Toast.info(err, 3, false) 73 | } 74 | } 75 | } 76 | 77 | export const homeList = (callback) => { 78 | return async (dispatch, getState) => { 79 | const { rank_id, locationInfo, shoplist } = getState().home // eslint-disable-line 80 | const location = { ...omit(locationInfo, ['address']) } 81 | try { 82 | const list = await getShopList({ 83 | ...location, 84 | rank_id: rank_id, // eslint-disable-line 85 | terminal: 'h5', 86 | offset: shoplist.length, 87 | limit: 8, 88 | extra_filters: 'home', 89 | extras: ['activities', 'tags'], 90 | }) 91 | dispatch(homeUpdate({ 92 | shoplist: [...shoplist, ...list.data.items], 93 | rank_id: list.data.meta.rank_id, 94 | })) 95 | callback && callback() // eslint-disable-line 96 | } catch ({ err }) { 97 | Toast.info(err, 3, false) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/pages/order/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import { connect } from 'react-redux' 5 | import { bindActionCreators } from 'redux' 6 | import { fetchOrderList } from 'stores/order' 7 | import Scroll from 'components/scroll' 8 | import NavBar from '../common-components/nav-bar' 9 | import withTabBar from '../common-components/tab-bar' 10 | import OrderRow from '../common-components/order-list-row' 11 | import AuthErr from '../common-components/auth-err' 12 | import RowSk from '../common-components/skeleton/row' 13 | import styles from './index.less' 14 | 15 | const mapStateToProps = ({ globalState, order }) => ({ 16 | isLogin: globalState.isLogin, 17 | init: order.init, 18 | orderList: order.orderList, 19 | }) 20 | 21 | const mapDispatchToProps = dispatch => bindActionCreators({ 22 | fetchOrderList, 23 | }, dispatch) 24 | 25 | @connect(mapStateToProps, mapDispatchToProps) 26 | @withTabBar 27 | export default class Order extends React.PureComponent { 28 | componentDidMount() { 29 | const { isLogin, init } = this.props 30 | if (isLogin && !init) { 31 | this.props.fetchOrderList(true, false) 32 | } 33 | } 34 | 35 | componentWillReceiveProps(nextProps) { 36 | if (nextProps.isLogin && !nextProps.init) { 37 | this.props.fetchOrderList(true, false) 38 | } 39 | } 40 | 41 | handlePullingDown = () => { 42 | this.props.fetchOrderList(true, () => { 43 | this.scroll && this.scroll.forceUpdate() // eslint-disable-line 44 | }) 45 | } 46 | 47 | handlePullingUp = () => { 48 | this.props.fetchOrderList(false, () => { 49 | this.scroll && this.scroll.forceUpdate() // eslint-disable-line 50 | }) 51 | } 52 | 53 | rowClick = (id) => { 54 | this.props.history.push({ 55 | pathname: '/order-detail', 56 | state: { id }, 57 | }) 58 | } 59 | 60 | render() { 61 | const { 62 | orderList, 63 | isLogin, 64 | init, 65 | } = this.props 66 | const scrollProps = { 67 | className: styles.scroll, 68 | dataSource: orderList, 69 | pullDownRefresh: { stop: 40 }, 70 | pullUpLoad: true, 71 | pullingDown: this.handlePullingDown, 72 | pullingUp: this.handlePullingUp, 73 | } 74 | 75 | return ( 76 |
77 | this.props.history.goBack()} /> 82 | { 83 | isLogin ? ( 84 | this.scroll = c}> 85 | { 86 | init ? orderList.map(v => ( 87 | this.rowClick(v.unique_id)} /> 88 | )) : Array.from({ length: 10 }, (v, i) => i).map(v => ( 89 | 90 | )) 91 | } 92 | 93 | ) : 94 | } 95 |
96 | ) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/components/toast/notification.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import Notice from './notices' 5 | import styles from './index.less' 6 | 7 | const prefixCls = 'notification' 8 | let noticeNumber = 0 9 | const getUuid = () => `notification-${+new Date()}-${noticeNumber++}` 10 | 11 | export default class Notification extends React.Component { 12 | constructor(props) { 13 | super(props) 14 | /** 15 | * notices 存放notices数组 16 | * hasMask 是否显示遮罩 17 | */ 18 | this.state = { 19 | notices: [], 20 | hasMask: true, 21 | } 22 | } 23 | 24 | // 创建noticeDom 25 | getNoticeDOM = () => { 26 | const { notices } = this.state 27 | return notices.map((notice) => { 28 | return 29 | }) 30 | } 31 | 32 | // 创建mask遮罩 33 | getMaskDOM = () => { 34 | const { notices, hasMask } = this.state 35 | // notices 不为空 && 始终只显示一个 mask 36 | if (notices.length && hasMask) { 37 | return
38 | } 39 | } 40 | 41 | // 添加 notice 方法 42 | add = (notice) => { 43 | const key = notice.key || getUuid() 44 | const mask = notice.mask || false 45 | // 排除重复因素后再添加 46 | this.setState((preState) => { 47 | const { notices } = preState 48 | if (!notices.find(v => v.key === key)) { 49 | return { 50 | notices: [...notices, { ...notice, key }], 51 | hasMask: mask, 52 | } 53 | } 54 | }) 55 | } 56 | 57 | // 移除notice 58 | remove = (key) => { 59 | this.setState((preState) => { 60 | return { 61 | notices: preState.notices.filter(v => v.key === key), 62 | } 63 | }) 64 | } 65 | 66 | render() { 67 | const noticesDOM = this.getNoticeDOM() 68 | const maskDOM = this.getMaskDOM() 69 | return ( 70 |
71 | {maskDOM} 72 |
73 | {noticesDOM} 74 |
75 |
76 | ) 77 | } 78 | } 79 | 80 | /** 81 | * Notification静态类方法 用于创建 Notification组件 以及返回他的方法和组件本身 82 | * properties 需要传递给 Notification的props 83 | * callback 用于接收 Notification各种方法的回调 84 | */ 85 | Notification.newInstance = (properties, callback) => { 86 | const { ...props } = properties || {} 87 | const div = document.createElement('div') 88 | document.body.appendChild(div) 89 | 90 | let called = false 91 | function ref(notification) { 92 | if (called) return 93 | called = true 94 | callback({ 95 | notice(noticeProps) { 96 | notification.add(noticeProps) 97 | }, 98 | removeNotice(key) { 99 | notification.remove(key) 100 | }, 101 | destroy() { 102 | ReactDOM.unmountComponentAtNode(div) 103 | div && div.parentNode.removeChild(div) // eslint-disable-line 104 | }, 105 | }) 106 | } 107 | ReactDOM.render(, div) 108 | } 109 | -------------------------------------------------------------------------------- /src/pages/shop-detail/cartcontrol/stepper.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import QueueAnim from 'rc-queue-anim' 5 | import cls from 'classnames' 6 | import { connect } from 'react-redux' 7 | import { bindActionCreators } from 'redux' 8 | import { shoppingCartUpdate } from 'stores/shopping-cart' 9 | import eventProxy from 'utils/event-proxy' 10 | import styles from './index.less' 11 | 12 | @connect(({ shoppingCart }) => ({ 13 | cart: shoppingCart.cart, 14 | }), dispatch => bindActionCreators({ 15 | shoppingCartUpdate, 16 | }, dispatch)) 17 | export default class Stepper extends React.PureComponent { 18 | increment = ({ target }) => { 19 | const { food, cart, dropBall = true } = this.props 20 | const specs = food.specfoods ? food.specfoods[0] : null 21 | const isHas = cart.find(v => v.virtual_food_id === food.virtual_food_id) 22 | if (!isHas && specs) { 23 | this.props.shoppingCartUpdate({ 24 | cart: [ 25 | ...cart, 26 | { 27 | attrs: [], 28 | quantity: 1, 29 | restaurant_id: food.restaurant_id, 30 | price: specs.price, 31 | new_specs: specs.specs, 32 | name: specs.name, 33 | food_id: specs.food_id, 34 | virtual_food_id: food.virtual_food_id, 35 | }, 36 | ], 37 | }) 38 | } else { 39 | const result = cart.map((v) => { 40 | if (v.virtual_food_id === food.virtual_food_id) { 41 | return { ...v, quantity: v.quantity + 1 } 42 | } 43 | return v 44 | }) 45 | this.props.shoppingCartUpdate({ cart: result }) 46 | } 47 | 48 | dropBall && eventProxy.trigger('cartBall', target) // eslint-disable-line 49 | } 50 | 51 | decrement = () => { 52 | const { food, cart } = this.props 53 | const result = cart.map((v) => { 54 | if (v.virtual_food_id === food.virtual_food_id) { 55 | return { ...v, quantity: v.quantity - 1 } 56 | } 57 | return v 58 | }).filter(v => v.quantity > 0) 59 | this.props.shoppingCartUpdate({ cart: result }) 60 | } 61 | 62 | render() { 63 | const { food, cart } = this.props 64 | const cartFood = cart.find(v => v.virtual_food_id === food.virtual_food_id) 65 | const count = cartFood ? cartFood.quantity : 0 66 | 67 | return ( 68 |
69 | 72 | { 73 | count > 0 ? ( 74 | 80 | ) : null 81 | } 82 | 83 | { 84 | count > 0 ? ( 85 |
{count}
86 | ) : null 87 | } 88 | 93 |
94 | ) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/pages/compass/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import { connect } from 'react-redux' 5 | import { bindActionCreators } from 'redux' 6 | import Scroll from 'components/scroll' 7 | import NavBar from '../common-components/nav-bar' 8 | import withTabBar from '../common-components/tab-bar' 9 | import AuthErr from '../common-components/auth-err' 10 | import RecommedFoodRow from '../common-components/recommend-food-row' 11 | import NoData from '../common-components/no-data' 12 | import RecommedSk from '../common-components/skeleton/recommend' 13 | import { fetchFoodList } from '../../stores/compass' 14 | import styles from './index.less' 15 | 16 | const mapStateToProps = ({ globalState, compass, home }) => ({ 17 | isLogin: globalState.isLogin, 18 | init: compass.init, 19 | foodList: compass.foodList, 20 | locationInfo: home.locationInfo, 21 | }) 22 | 23 | const mapDispatchToProps = dispatch => bindActionCreators({ 24 | fetchFoodList, 25 | }, dispatch) 26 | 27 | @connect(mapStateToProps, mapDispatchToProps) 28 | @withTabBar 29 | export default class Compass extends React.Component { 30 | componentDidMount() { 31 | const { isLogin, init } = this.props 32 | if (isLogin && !init) { 33 | this.props.fetchFoodList() 34 | } 35 | } 36 | 37 | componentWillReceiveProps(nextProps) { 38 | if (nextProps.isLogin && !nextProps.init) { 39 | this.props.fetchFoodList() 40 | } 41 | } 42 | 43 | handleRowClick = (val) => { 44 | const { history, locationInfo } = this.props 45 | const { id } = val 46 | const { latitude, longitude } = locationInfo 47 | history.push({ 48 | pathname: '/shop-detail', 49 | search: `?restaurant_id=${id}&latitude=${latitude}&longitude=${longitude}`, 50 | }) 51 | } 52 | 53 | render() { 54 | const { 55 | isLogin, 56 | foodList, 57 | init, 58 | history, 59 | } = this.props 60 | 61 | const scrollProps = { 62 | className: styles.scroll, 63 | dataSource: foodList, 64 | pullUpLoad: true, 65 | pullingUp: this.props.fetchFoodList, 66 | } 67 | 68 | return ( 69 |
70 | history.goBack()} /> 74 | { 75 | isLogin && init ? ( 76 | 77 | { 78 | foodList.length ? foodList.map((v, i) => ( 79 | this.handleRowClick(v.restaurant)} /> 83 | )) : 84 | } 85 | 86 | ) : isLogin && !init ? ( 87 |
88 | { 89 | Array.from({ length: 20 }, (v, i) => i).map(v => ( 90 | 91 | )) 92 | } 93 |
94 | ) : 95 | } 96 |
97 | ) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/pages/shop-detail/shop/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import { connect } from 'react-redux' 5 | import { getImageUrl } from 'utils/utils' 6 | import Scroll from 'components/scroll' 7 | import Badge from '../../common-components/badge' 8 | import styles from './index.less' 9 | 10 | @connect(({ shop }) => ({ 11 | info: shop.info, 12 | })) 13 | export default class ShopInfo extends React.PureComponent { 14 | render() { 15 | const { show, info } = this.props 16 | const flavors = info.flavors.length ? info.flavors.map(v => v.name).join(',') : '--' 17 | const opening_hours = info.opening_hours.length ? info.opening_hours.join(',') : '--' 18 | 19 | return !show ? null : ( 20 |
21 | 22 |
23 |

配送信息

24 |

{`由蜂鸟快送提供配送,约${info.order_lead_time}分钟送达,距离${info.distance}m`}

25 |

{info.piecewise_agent_fee ? info.piecewise_agent_fee.description : ''}

26 |
27 | 28 |
29 |

活动与服务

30 | { 31 | info.activities ? info.activities.map(v => ( 32 |
33 | 37 | {v.tips} 38 |
39 | )) :

暂无活动

40 | } 41 |
42 | 43 |
44 |

商家实景

45 |
46 | { 47 | info.albums ? info.albums.map((img, i) => ( 48 |
49 | 50 |
51 | )) :

暂无实景

52 | } 53 |
54 |
55 | 56 |
57 |

商家信息

58 |

{info.description}

59 |
60 |
品类
61 |
{flavors}
62 |
63 |
64 |
商家电话
65 |
{info.phone}
66 |
67 |
68 |
地址
69 |
{info.address}
70 |
71 |
72 |
营业时间
73 |
{opening_hours}
74 |
75 |
76 |
77 |
78 | ) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/pages/common-components/order-list-row/index.less: -------------------------------------------------------------------------------- 1 | 2 | @import '../../../assets/css/mixin.less'; 3 | @import '../../../assets/css/theme.less'; 4 | 5 | .order-row { 6 | background-color: @fill-body; 7 | margin-bottom: 20px; 8 | box-sizing: border-box; 9 | padding: 28px 0 0 30px; 10 | 11 | .line { 12 | width: 100%; 13 | height: 1px; 14 | margin-top: 20px; 15 | } 16 | 17 | .order-body { 18 | display: flex; 19 | .shop-img { 20 | flex: 0 0 64px; 21 | width: 64px; 22 | margin-right: 24px; 23 | img { 24 | width: 100%; 25 | } 26 | } 27 | .info-wrapper { 28 | flex: 1; 29 | .shop-info { 30 | display: flex; 31 | height: 48px; 32 | box-sizing: border-box; 33 | padding-right: 30px; 34 | align-items: center; 35 | .shop { 36 | flex: 1; 37 | font-size: 0; 38 | .name { 39 | display: inline-block; 40 | vertical-align: middle; 41 | max-width: 300px; 42 | .no-wrap; 43 | font-size: @font-size-medium-x; 44 | color: @color-title; 45 | } 46 | .icon { 47 | display: inline-block; 48 | vertical-align: middle; 49 | color: @color-base; 50 | font-size: @font-size-small-s; 51 | } 52 | } 53 | .status { 54 | flex: 0 0 260px; 55 | width: 260px; 56 | .no-wrap; 57 | text-align: right; 58 | font-size: @font-size-small; 59 | font-weight: @font-weight-small; 60 | color: @color-title; 61 | } 62 | } 63 | .time { 64 | margin: 8px 0 0; 65 | font-size: @font-size-small; 66 | font-weight: @font-weight-small; 67 | color: @color-base; 68 | transform: scale(.8); 69 | transform-origin: 0; 70 | } 71 | 72 | .order-detail { 73 | display: flex; 74 | height: 80px; 75 | line-height: 80px; 76 | padding-right: 30px; 77 | box-sizing: border-box; 78 | justify-content: center; 79 | align-items: space-between; 80 | .desc { 81 | flex: 1; 82 | width: 0; 83 | .no-wrap; 84 | font-size: @font-size-small; 85 | font-weight: @font-weight-small; 86 | color: @color-base; 87 | margin-right: 40px; 88 | } 89 | .price { 90 | font-size: @font-size-small; 91 | color: #000; 92 | } 93 | } 94 | } 95 | } 96 | 97 | .order-footer { 98 | display: flex; 99 | height: 100px; 100 | padding-right: 30px; 101 | box-sizing: border-box; 102 | align-items: center; 103 | justify-content: flex-end; 104 | .more { 105 | padding: 0; 106 | display: block; 107 | background-color: #fff; 108 | border-radius: 4px; 109 | outline: none; 110 | border: none; 111 | width: 146px; 112 | height: 60px; 113 | line-height: 60px; 114 | font-size: @font-size-small; 115 | font-weight: @font-weight-small; 116 | border: 1px solid @primary-color-light; 117 | color: @primary-color; 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/pages/search-shop/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { connect } from 'react-redux' 4 | import { bindActionCreators } from 'redux' 5 | import { searchShopDestroy, getHotKeys, searchShopUpdate, getShopList } from 'stores/search-shop' 6 | import SvgIcon from 'components/icon-svg' 7 | import NavBar from '../common-components/nav-bar' 8 | import ShopList from './list' 9 | import styles from './index.less' 10 | 11 | const mapStateToProps = ({ searchShop, home }) => ({ 12 | keywords: searchShop.keywords, 13 | hotKeys: searchShop.hotKeys, 14 | locationInfo: home.locationInfo, 15 | }) 16 | const mapDispatchToProps = dispatch => bindActionCreators({ 17 | searchShopDestroy, 18 | getHotKeys, 19 | searchShopUpdate, 20 | getShopList, 21 | }, dispatch) 22 | 23 | @connect(mapStateToProps, mapDispatchToProps) 24 | export default class SearchShop extends React.Component { 25 | componentDidMount() { 26 | this.props.getHotKeys() 27 | } 28 | 29 | componentWillUnmount() { 30 | this.props.searchShopDestroy() 31 | if (this.timer) { 32 | clearTimeout(this.timer) 33 | this.timer = null 34 | } 35 | } 36 | 37 | searhChange = ({ target }) => { 38 | const { locationInfo } = this.props 39 | this.props.searchShopUpdate({ 40 | keywords: target.value, 41 | }) 42 | if (!locationInfo.latitude && !locationInfo.longitude) { 43 | return 44 | } 45 | if (this.timer) clearTimeout(this.timer) 46 | this.timer = setTimeout(this.props.getShopList, 600) 47 | } 48 | 49 | badgeClick = (val) => { 50 | const { locationInfo } = this.props 51 | this.props.searchShopUpdate({ 52 | keywords: val, 53 | }) 54 | if (!locationInfo.latitude && !locationInfo.longitude) { 55 | return 56 | } 57 | this.props.getShopList() 58 | } 59 | 60 | searchClick = () => { 61 | const { locationInfo } = this.props 62 | if (!locationInfo.latitude && !locationInfo.longitude) { 63 | return 64 | } 65 | this.props.getShopList() 66 | } 67 | 68 | render() { 69 | const { keywords, hotKeys } = this.props 70 | 71 | return ( 72 |
73 | this.props.history.goBack()} /> 77 | 78 |
79 |
80 | 81 | 82 |
83 | 84 |
85 | 86 | { 87 | !keywords ? ( 88 |
89 |

热门搜索

90 | { 91 | hotKeys.map((v, i) => ( 92 |
this.badgeClick(v.word)}> 96 | {v.word} 97 |
98 | )) 99 | } 100 |
101 | ) : 102 | } 103 | 104 |
105 | ) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/pages/order-detail/index.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | @import '../../assets/css/theme.less'; 4 | @import '../../assets/css/mixin.less'; 5 | 6 | .detail { 7 | position: fixed; 8 | z-index: 1; 9 | .position-full; 10 | background-color: @fill-body-darken; 11 | .scroll { 12 | top: @bar-height; 13 | background-color: @fill-body-darken; 14 | margin: 30px; 15 | overflow: visible; 16 | z-index: -1; 17 | .content { 18 | box-sizing: border-box; 19 | padding: 0 20px; 20 | box-shadow: 0 3px 5px rgba(0,0,0,.05); 21 | background-color: @fill-body; 22 | .item { 23 | display: flex; 24 | min-height: 88px; 25 | align-items: center; 26 | box-shadow: 0 1px 1px -1px @border-color; 27 | .img { 28 | flex: 0 0 40px; 29 | height: 40px; 30 | border-radius: 40px; 31 | background-color: @fill-body-darken; 32 | margin-right: 20px; 33 | img { 34 | width: 100%; 35 | border-radius: 100%; 36 | } 37 | } 38 | .text { 39 | flex: 1; 40 | width: 0; 41 | .no-wrap; 42 | font-size: @font-size-medium; 43 | font-weight: @font-weight-small; 44 | line-height: 40px; 45 | color: @color-title; 46 | } 47 | .num,.price { 48 | text-align: right; 49 | font-size: @font-size-small; 50 | font-weight: @font-weight-small; 51 | line-height: 40px; 52 | color: @color-title; 53 | &.red { 54 | color: #ff5339; 55 | } 56 | } 57 | .num { 58 | flex: 0 0 80px; 59 | width: 60px; 60 | } 61 | .price { 62 | flex: 0 0 130px; 63 | width: 100px; 64 | } 65 | } 66 | .total { 67 | text-align: right; 68 | font-size: @font-size-large; 69 | color: @color-base; 70 | line-height: 100px; 71 | } 72 | } 73 | .info { 74 | margin-top: 30px; 75 | box-shadow: 0 3px 5px rgba(0,0,0,.05); 76 | background-color: @fill-body; 77 | .title { 78 | box-sizing: border-box; 79 | padding: 0 30px; 80 | box-shadow: 0 1px 1px -1px @border-color; 81 | line-height: 88px; 82 | font-size: @font-size-medium; 83 | font-weight: @font-weight-small; 84 | color: @color-title; 85 | } 86 | .desc { 87 | box-sizing: border-box; 88 | padding-left: 30px; 89 | .item { 90 | display: flex; 91 | min-height: 68px; 92 | padding: 10px 0; 93 | align-items: center; 94 | box-shadow: 0 1px 1px -1px @border-color; 95 | .label { 96 | flex: 0 0 130px; 97 | width: 130px; 98 | margin-right: 20px; 99 | font-size: @font-size-small; 100 | font-weight: @font-weight-small; 101 | line-height: 40px; 102 | color: @color-title; 103 | } 104 | .text { 105 | flex: 1; 106 | width: 0; 107 | font-size: @font-size-small; 108 | font-weight: @font-weight-small; 109 | line-height: 40px; 110 | color: @color-base; 111 | .no-wrap; 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | import HttpUtils from './http' 4 | 5 | const position = new AMap.Geolocation({ 6 | enableHighAccuracy: true, 7 | maximumAge: 0, 8 | convert: true, 9 | }) 10 | 11 | export const getGeolocation = () => { 12 | return new Promise((resolve, reject) => { 13 | position.getCurrentPosition((status, result) => { 14 | if (status === 'complete') { 15 | resolve({ 16 | data: { 17 | latitude: result.position.lat, 18 | longitude: result.position.lng, 19 | address: result.formattedAddress, 20 | }, 21 | }) 22 | } else { 23 | reject({ 24 | err: result.message, 25 | }) 26 | } 27 | }) 28 | }) 29 | } 30 | export const getEntry = (params) => { return HttpUtils.get('/elm/entry', params) } 31 | export const getBanner = (params) => { return HttpUtils.get('/elm/banner', params) } 32 | export const getShopList = (params) => { return HttpUtils.get('/elm/restaurants', params) } 33 | // 通过关键字搜索商家 34 | export const getShopListByKw = (params) => { return HttpUtils.get('/elm/restaurants_search', params) } 35 | 36 | export const getOrderList = (params) => { return HttpUtils.get('/elm/orders', params) } 37 | export const getOrderSnapshot = (params) => { return HttpUtils.get('/elm/order-snapshot', params) } 38 | export const getOrderDesc = (params) => { return HttpUtils.get('/elm/order-desc', params) } 39 | 40 | // 商店 41 | export const getShopInfo = (params) => { return HttpUtils.get('/elm/restaurant_byid', params) } 42 | export const getShopRatings = (params) => { return HttpUtils.get('/elm/restaurant_ratings', params) } 43 | export const getShopFood = (params) => { return HttpUtils.get('/elm/restaurant_menu', params) } 44 | export const getRatingTags = (params) => { return HttpUtils.get('/elm/rating_tags', params) } 45 | export const getRatingScores = (params) => { return HttpUtils.get('/elm/rating_scores', params) } 46 | 47 | export const getTotalCategory = (params) => { return HttpUtils.get('/elm/total_category', params) } 48 | export const getFoodSiftFactors = (params) => { return HttpUtils.get('/elm/food_sift_factors', params) } 49 | export const getFilterAttr = (params) => { return HttpUtils.get('/elm/filter_attributes', params) } 50 | 51 | // 热门关键词 52 | export const getHotKeywords = (params) => { return HttpUtils.get('/elm/hot_keywords', params) } 53 | // 推荐食物 54 | export const getRecommendation = (params) => { return HttpUtils.get('/elm/recommendation', params) } 55 | 56 | // 登陆 用户信息 57 | export const mobileSendCode = (params) => { return HttpUtils.post('/elm/mobile_send_code', params) } 58 | export const mobileCaptchas = (params) => { return HttpUtils.post('/elm/captchas', params) } 59 | export const loginByMobile = (params) => { return HttpUtils.post('/elm/login_by_mobile', params) } 60 | export const getUserInfo = (params) => { return HttpUtils.get('/elm/users', params) } 61 | 62 | export const getAddress = (params) => { return HttpUtils.get('/elm/address', params) } 63 | export const delAddress = (params) => { return HttpUtils.get('/elm/del_address', params) } 64 | export const upAddress = (params) => { return HttpUtils.post('/elm/update_address', params) } 65 | export const addAddress = (params) => { return HttpUtils.post('/elm/add_address', params) } 66 | 67 | export const getHongbaos = (params) => { return HttpUtils.get('/elm/hongbaos', params) } 68 | 69 | // 根据经纬度 关键词 获取地址 70 | export const getNearby = (params) => { return HttpUtils.get('/elm/search_nearby', params) } 71 | -------------------------------------------------------------------------------- /src/assets/svg/new.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/drumstick.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/common-components/red-ticket-row/index.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | @import '../../../assets/css/mixin.less'; 4 | @import '../../../assets/css/theme.less'; 5 | 6 | .row { 7 | width: 100%; 8 | background-color: @fill-body; 9 | margin-bottom: 20px; 10 | border-radius: 4px; 11 | overflow: hidden; 12 | box-shadow: 0px 6px 5px -5px rgba(0,0,0,.1); 13 | .body { 14 | width: 100%; 15 | height: 168px; 16 | background-color: @fill-body; 17 | display: flex; 18 | position: relative; 19 | &::after, &::before{ 20 | position: absolute; 21 | content: '' !important; 22 | display: block; 23 | border: 12px solid @fill-body-darken; 24 | border-radius: 100%; 25 | z-index: 2; 26 | } 27 | &::after { 28 | left: -12px; 29 | bottom: -12px; 30 | } 31 | &::before { 32 | right: -12px; 33 | bottom: -12px; 34 | } 35 | .icon { 36 | position: absolute; 37 | font-size: @font-size-large-x; 38 | top: 0; 39 | left: 0; 40 | color: @badge-color-open; 41 | } 42 | .left { 43 | flex: 0 0 190px; 44 | width: 190px; 45 | display: flex; 46 | flex-direction: column; 47 | justify-content: center; 48 | .amount { 49 | text-align: center; 50 | color: #ff0025; 51 | font-size: @font-size-large-x; 52 | span { 53 | font-weight: 700; 54 | } 55 | .unit { 56 | font-size: @font-size-small; 57 | margin-right: 10px; 58 | } 59 | } 60 | .desc { 61 | font-size: @font-size-small; 62 | font-weight: @font-weight-small; 63 | color: @color-base; 64 | text-align: center; 65 | transform: scale(.9); 66 | transform-origin: center center; 67 | margin-top: 10px; 68 | } 69 | } 70 | .center { 71 | flex: 1; 72 | width: 0; 73 | display: flex; 74 | flex-direction: column; 75 | justify-content: center; 76 | margin: 0 10px; 77 | .title { 78 | font-size: @font-size-medium; 79 | color: @color-title; 80 | margin-bottom: 10px; 81 | } 82 | .desc { 83 | font-size: @font-size-small; 84 | font-weight: @font-weight-small; 85 | color: @color-base; 86 | transform: scale(.9); 87 | margin-bottom: 6px; 88 | transform-origin: 0; 89 | } 90 | } 91 | .right { 92 | flex: 0 0 150px; 93 | width: 150px; 94 | display: flex; 95 | flex-direction: column; 96 | justify-content: center; 97 | .title { 98 | text-align: center; 99 | font-size: @font-size-small; 100 | color: #ff0025; 101 | margin-bottom: 10px; 102 | } 103 | .btn { 104 | outline: none; 105 | margin: 0 auto; 106 | border: none; 107 | text-align: center; 108 | width: 110px; 109 | line-height: 46px; 110 | color: #fff; 111 | background-color: #ff0025; 112 | border-radius: 46px; 113 | } 114 | } 115 | } 116 | .bottom { 117 | width: 100%; 118 | height: 74px; 119 | background-color: #fcfcfc; 120 | box-sizing: border-box; 121 | border-top: 1px dotted @border-color; 122 | padding: 0 30px; 123 | .desc { 124 | width: 100%; 125 | height: 100%; 126 | position: relative; 127 | span { 128 | position: absolute; 129 | left: 0; 130 | right: -120px; 131 | top: 50%; 132 | display: block; 133 | font-size: @font-size-small; 134 | font-weight: @font-weight-small; 135 | transform: scale(.8) translateY(-50%); 136 | transform-origin: 0 0; 137 | color: @color-base; 138 | .no-wrap; 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/pages/login/index.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | @import '../../assets/css/theme.less'; 4 | @import '../../assets/css/mixin.less'; 5 | 6 | .login { 7 | position: fixed; 8 | .position-full; 9 | background-color: @fill-body; 10 | .logo { 11 | font-size: 260px; 12 | text-align: center; 13 | } 14 | .form { 15 | width: 600px; 16 | margin: 0 auto; 17 | .item { 18 | width: 100%; 19 | height: @input-height; 20 | border: 1px solid #ddd; 21 | border-radius: 8px; 22 | margin-bottom: 40px; 23 | overflow: hidden; 24 | position: relative; 25 | .placeholder-color(#ababab); 26 | .code-btn { 27 | height: @input-height; 28 | line-height: @input-height; 29 | font-size: @font-size-medium; 30 | font-weight: @font-weight-small; 31 | color: #cfcfcf; 32 | background-color: transparent; 33 | outline: none; 34 | position: absolute; 35 | border: none; 36 | right: 0; 37 | top: 0; 38 | } 39 | input { 40 | width: 100%; 41 | height: 100%; 42 | padding-left: 20px; 43 | font-size: @font-size-medium; 44 | font-weight: @font-weight-small; 45 | outline: none; 46 | color: #333; 47 | } 48 | } 49 | } 50 | .desc { 51 | font-size: @font-size-small; 52 | color: #ababab; 53 | width: 600px; 54 | margin: 0 auto; 55 | line-height: 1.6em; 56 | span { 57 | color: @primary-color-light; 58 | } 59 | } 60 | .login-btn { 61 | margin: 0 auto; 62 | display: block; 63 | background-color: #4cd96f; 64 | border-radius: 8px; 65 | outline: none; 66 | border: none; 67 | width: 600px; 68 | height: 84px; 69 | line-height: 84px; 70 | margin-top: 40px; 71 | font-size: @font-size-medium-x; 72 | font-weight: @font-weight-small; 73 | color:#fff; 74 | } 75 | } 76 | 77 | 78 | .captcha-modal { 79 | position: fixed; 80 | .position-full; 81 | z-index: 1; 82 | .mask { 83 | position: absolute; 84 | .position-full; 85 | z-index: 1; 86 | background-color: rgba(0,0,0,0.5); 87 | } 88 | .body { 89 | position: absolute; 90 | top: 50%; 91 | left: 50%; 92 | transform: translate3d(-50%, -50%, 0); 93 | z-index: 2; 94 | background-color: @fill-body; 95 | border-radius: 6px; 96 | padding: 30px 20px; 97 | width: 500px; 98 | .title { 99 | font-size: @font-size-medium-x; 100 | color: @color-title; 101 | line-height: 40px; 102 | text-align: center; 103 | margin-bottom: 30px; 104 | } 105 | .item { 106 | width: 100%; 107 | height: @input-height - 20; 108 | border: 1px solid #ddd; 109 | border-radius: 8px; 110 | overflow: hidden; 111 | margin-bottom: 30px; 112 | position: relative; 113 | .placeholder-color(#ababab); 114 | input { 115 | width: 100%; 116 | height: 100%; 117 | padding-left: 20px; 118 | box-sizing: border-box; 119 | font-size: @font-size-medium; 120 | font-weight: @font-weight-small; 121 | outline: none; 122 | color: #333; 123 | } 124 | .img { 125 | position: absolute; 126 | content: none !important; 127 | right: 6px; 128 | top: 10px; 129 | width: 160px; 130 | height: @input-height - 40; 131 | } 132 | } 133 | } 134 | .footer { 135 | border-top: 1px solid @border-color; 136 | font-size: 0; 137 | .reset, .submit { 138 | display: inline-block; 139 | width: 50%; 140 | height: 80px; 141 | line-height: 80px; 142 | text-align: center; 143 | background-color: @fill-body-darken; 144 | font-size: @font-size-medium; 145 | font-weight: @font-weight-small; 146 | color: @color-title; 147 | } 148 | .submit { 149 | color: #fff; 150 | background-color: #4cd96f; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/assets/svg/shop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/profile/index.less: -------------------------------------------------------------------------------- 1 | 2 | 3 | @import '../../assets/css/theme.less'; 4 | @import '../../assets/css/mixin.less'; 5 | 6 | .root { 7 | width: 100%; 8 | height: 100%; 9 | background-color: @fill-body-darken; 10 | 11 | .profile-info { 12 | width: 100%; 13 | height: 200px; 14 | box-sizing: border-box; 15 | padding: 10px 50px 30px; 16 | background-image: linear-gradient(-90deg, @primary-color, @primary-color-light); 17 | display: flex; 18 | position: relative; 19 | .flex-center; 20 | .icon-right { 21 | position: absolute; 22 | right: 20px; 23 | top: 50%; 24 | transform: translateY(-50%); 25 | font-size: @font-size-medium; 26 | color: #fff; 27 | } 28 | .avatar { 29 | flex: 0 0 120px; 30 | width: 120px; 31 | height: 120px; 32 | border-radius: 100%; 33 | background-color: @fill-body; 34 | overflow: hidden; 35 | position: relative; 36 | color: #dcdcdc; 37 | font-size: 120px; 38 | .icon { 39 | position: absolute; 40 | left: 50%; 41 | transform: translateX(-50%); 42 | bottom: -8px; 43 | } 44 | .img { 45 | width: 120px; 46 | height: 120px; 47 | content: none !important; 48 | } 49 | } 50 | .desc { 51 | flex: 1; 52 | margin-left: 36px; 53 | .info { 54 | font-size: @font-size-large-x; 55 | color: #fff; 56 | font-weight: @font-weight-medium; 57 | margin-bottom: 16px; 58 | } 59 | .text { 60 | font-size: 0; 61 | .icon, span { 62 | display: inline-block; 63 | vertical-align: bottom; 64 | color: #fff; 65 | font-size: @font-size-small; 66 | font-weight: @font-weight-small; 67 | } 68 | .icon { 69 | font-size: @font-size-medium; 70 | } 71 | } 72 | } 73 | } 74 | 75 | .column { 76 | width: 100%; 77 | height: 160px; 78 | background-color: @fill-body; 79 | border-bottom: 1px solid @border-color; 80 | margin-bottom: 20px; 81 | display: flex; 82 | .item { 83 | flex: 1; 84 | border-right: 1px solid @border-color; 85 | &:last-child { 86 | border-right: none; 87 | } 88 | display: flex; 89 | .flex-center; 90 | flex-direction: column; 91 | .icon { 92 | display: flex; 93 | .flex-center; 94 | width: 68px; 95 | height: 68px; 96 | background-color: rgba(0, 0, 0, .1); 97 | font-size: @font-size-large-x; 98 | border-radius: 100%; 99 | margin-bottom: 10px; 100 | } 101 | .count { 102 | font-size: @font-size-large-x; 103 | margin-bottom: 10px; 104 | &.blue { 105 | color: rgb(0, 152, 251); 106 | } 107 | &.red { 108 | color: rgb(255, 95, 62); 109 | } 110 | &.green { 111 | color: rgb(106, 194, 11); 112 | } 113 | .unit { 114 | margin-left: 6px; 115 | font-size: @font-size-medium; 116 | } 117 | } 118 | .desc { 119 | font-size: @font-size-small; 120 | color: @color-base; 121 | } 122 | } 123 | } 124 | 125 | .list { 126 | background-color: @fill-body; 127 | border-top: 1px solid @border-color; 128 | &:last-child { 129 | border-bottom: 1px solid @border-color; 130 | } 131 | .item { 132 | background-color: @fill-body; 133 | display: flex; 134 | .flex-center; 135 | height: @item-height; 136 | position: relative; 137 | .icon { 138 | flex: 0 0 @item-height; 139 | height: @item-height; 140 | display: flex; 141 | .flex-center; 142 | font-size: @font-size-large-x; 143 | } 144 | .desc { 145 | flex: 1; 146 | height: @item-height; 147 | line-height: @item-height; 148 | font-size: @font-size-medium; 149 | } 150 | .icon-right { 151 | position: absolute; 152 | right: 20px; 153 | top: 50%; 154 | transform: translateY(-50%); 155 | font-size: @font-size-medium; 156 | color: @color-base; 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/assets/svg/gold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/profile/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import React from 'react' 4 | import { connect } from 'react-redux' 5 | import cls from 'classnames' 6 | import SvgIcon from 'components/icon-svg' 7 | import { formatPhone, getImageUrl } from 'utils/utils' 8 | import NavBar from '../common-components/nav-bar' 9 | import withTabBar from '../common-components/tab-bar' 10 | import styles from './index.less' 11 | 12 | @connect(({ globalState }) => ({ 13 | isLogin: globalState.isLogin, 14 | userInfo: globalState.userInfo, 15 | })) 16 | @withTabBar 17 | export default class Profile extends React.Component { 18 | render() { 19 | const { history, userInfo, isLogin } = this.props 20 | const { 21 | username, 22 | mobile, 23 | avatar, 24 | balance, 25 | brand_member_new, 26 | gift_amount, 27 | } = userInfo 28 | const avatarUrl = getImageUrl(avatar) 29 | const changePage = path => history.push(path) 30 | 31 | const getItemContent = (icon, style, count, unit) => { 32 | if (isLogin) { 33 | return ( 34 |
35 | {count} 36 | {unit} 37 |
38 | ) 39 | } 40 | return ( 41 |
42 | 43 |
44 | ) 45 | } 46 | 47 | const goDetail = () => { 48 | console.log('123123') 49 | } 50 | 51 | return ( 52 |
53 | this.props.history.goBack()} /> 57 | 58 |
changePage('/login') : goDetail}> 59 |
60 | { 61 | avatarUrl ? ( 62 | 63 | ) : 64 | } 65 |
66 |
67 |

68 | { 69 | !isLogin ? '登陆/注册' : username 70 | } 71 |

72 |

73 | 74 | 75 | { 76 | !isLogin ? '登陆后享受更多特权' : formatPhone(mobile) 77 | } 78 | 79 |

80 |
81 | 82 |
83 | 84 |
85 |
86 | { 87 | getItemContent('#purse', styles.blue, balance, '元') 88 | } 89 |

钱包

90 |
91 |
changePage('/benefit')}> 92 | { 93 | getItemContent('#red-packet', styles.red, gift_amount, '个') 94 | } 95 |

红包

96 |
97 |
98 | { 99 | getItemContent('#gold', styles.green, brand_member_new, '个') 100 | } 101 |

金币

102 |
103 |
104 | 105 |
106 |
changePage('/order')}> 107 |
108 | 109 |
110 |

我的订单

111 | 112 |
113 |
114 | 115 |
116 |
changePage('/address')}> 117 |
118 | 119 |
120 |

我的地址

121 | 122 |
123 |
124 | 125 |
126 | ) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/components/vertical-slide/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | /*eslint-disable*/ 3 | import React from 'react' 4 | import BScroll from 'better-scroll' 5 | import PropTypes from 'prop-types' 6 | import { addClass } from 'utils/dom' 7 | import styles from './index.less' 8 | 9 | const slideProps = { 10 | loop: PropTypes.bool, 11 | autoPlay: PropTypes.bool, 12 | interval: PropTypes.number, 13 | click: PropTypes.bool, 14 | } 15 | 16 | const defaultSlideProps = { 17 | loop: true, 18 | autoPlay: true, 19 | interval: 900, 20 | click: true, 21 | } 22 | 23 | export default class VerticalSlide extends React.Component { 24 | static defaultProps = defaultSlideProps 25 | static propTypes = slideProps 26 | 27 | constructor(props) { 28 | super(props) 29 | this.state = { 30 | currentPageIndex: 0 31 | } 32 | this.children = [] 33 | this.timer = null 34 | this.resizeTimer = null 35 | } 36 | 37 | componentDidMount() { 38 | this.update() 39 | window.addEventListener('resize', () => { 40 | if (!this.slide || !this.slide.enabled) return 41 | this.resizeTimer && clearTimeout(this.resizeTimer) 42 | this.resizeTimer = setTimeout(() => { 43 | if (this.slide.isInTransition) { 44 | this.onScrollEnd() 45 | } else { 46 | this.props.autoPlay && this.play() 47 | } 48 | this.refresh() 49 | }, 60) 50 | }, false) 51 | } 52 | 53 | componentWillUnmount() { 54 | this.slide.disable() 55 | this.slide.destroy() 56 | if (this.timer) { 57 | clearTimeout(this.timer) 58 | this.timer = null 59 | } 60 | if (this.resizeTimer) { 61 | clearTimeout(this.resizeTimer) 62 | this.resizeTimer = null 63 | } 64 | } 65 | 66 | update = () => { 67 | if (this.slide) { 68 | this.slide.destroy() 69 | } 70 | this.init() 71 | } 72 | 73 | refresh = () => { 74 | this.setSlideHeight(true) 75 | this.slide.refresh() 76 | } 77 | 78 | next = () => { 79 | this.slide.next() 80 | } 81 | 82 | init = () => { 83 | if (!this.wrapper) return 84 | this.timer && clearTimeout(this.timer) 85 | this.setState({ currentPageIndex: 0 }) 86 | // 设置容器高度 87 | this.setSlideHeight() 88 | // 初始化slide 89 | this.initSldie() 90 | } 91 | 92 | setSlideHeight = (isResize = false) => { 93 | this.children = this.slideGroup.childNodes 94 | let height = 0 95 | let slideHeight = this.wrapper.clientHeight 96 | this.children.forEach(child => { 97 | if (child.nodeType === 1) { 98 | // 添加默认样式 99 | addClass(child, styles['slide-item']) 100 | child.style.height = `${slideHeight}px` 101 | height += slideHeight 102 | } 103 | }) 104 | if (this.props.loop && !isResize) { 105 | height += 2 * slideHeight 106 | } 107 | this.slideGroup.style.height = `${height}px` 108 | } 109 | 110 | initSldie = () => { 111 | const { loop, autoPlay, click } = this.props 112 | this.slide = new BScroll(this.wrapper, { 113 | click, 114 | scrollX: false, 115 | scrollY: true, 116 | momentum: false, 117 | snap: { 118 | loop, 119 | threshold: 0.3, 120 | speed: 400, 121 | }, 122 | bounce: false, 123 | }) 124 | 125 | this.slide.on('scrollEnd', this.onScrollEnd) 126 | 127 | this.slide.on('touchEnd', () => { 128 | this.props.autoPlay && this.play() 129 | }) 130 | 131 | this.slide.on('beforeScrollStart', () => { 132 | this.props.autoPlay && this.timer && clearTimeout(this.timer) 133 | }) 134 | 135 | if (this.props.autoPlay) { 136 | this.play() 137 | } 138 | } 139 | 140 | onScrollEnd = () => { 141 | let pageIndex = this.slide.getCurrentPage().pageY 142 | this.setState({ currentPageIndex: pageIndex }) 143 | this.props.autoPlay && this.play() 144 | } 145 | 146 | play = () => { 147 | this.timer && clearTimeout(this.timer) 148 | this.timer = setTimeout(() => { 149 | this.slide.next() 150 | }, this.props.interval) 151 | } 152 | 153 | render() { 154 | const { children } = this.props 155 | return ( 156 |
this.wrapper =c}> 157 |
this.slideGroup = c}> 158 | {children} 159 |
160 |
161 | ) 162 | } 163 | } 164 | --------------------------------------------------------------------------------